[{"data":1,"prerenderedAt":10313},["ShallowReactive",2],{"writes-\u002Fwrites\u002Fanatomy-of-adatatable":3,"related-writes-\u002Fwrites\u002Fanatomy-of-adatatable":4057},{"id":4,"title":5,"body":6,"date":4036,"description":4037,"description_ar":4038,"draft":4039,"extension":4040,"image":4041,"keywords":4042,"lang":4050,"meta":4051,"navigation":115,"path":4052,"seo":4053,"stem":4054,"title_ar":4055,"updatedAt":4036,"__hash__":4056},"writes\u002Fwrites\u002Fanatomy-of-adatatable.md","Anatomy of ADataTable, a typed generic Vue data table",{"type":7,"value":8,"toc":4013},"minimark",[9,18,25,28,36,39,170,181,184,492,495,507,525,540,547,560,579,585,743,775,781,863,889,910,916,925,942,1153,1156,1203,1206,1211,1319,1325,1347,1355,1377,1384,1393,1604,1609,1763,1777,1794,1798,1810,1813,1882,1885,2158,2161,2210,2227,2234,2245,2334,2337,2439,2442,2464,2471,2478,2550,2564,2572,2579,2699,2705,2721,2735,2748,2764,2767,2827,2830,2834,2837,2876,2883,2886,3011,3018,3021,3277,3284,3287,3298,3316,3418,3431,3441,3446,3565,3568,3575,3662,3669,3673,3676,3682,3738,3743,3746,3817,3831,3838,3912,3925,3929,3932,3997,4009],[10,11,12,13,17],"p",{},"I've written the same data table three times. Once as a ball of JSX, once as a class-based configuration object called ",[14,15,16],"code",{},"IColumn",", and once, the one that stuck, as a typed generic Vue component with a schema-shaped column type. The third time, it finally stopped being the thing I dreaded touching.",[10,19,20,21,24],{},"This post is a tour of the current shape. It lives in my UI package as ",[14,22,23],{},"ADataTable",", and the interesting thing about it is not any single trick. It's that the component has stopped growing. Every feature request for the last year has fit into the existing abstractions without a new prop. That's the version of \"done\" I was aiming for.",[10,26,27],{},"I'll walk the files in roughly the order the request flows through them, pausing on the bits worth explaining.",[29,30,32,33,35],"h2",{"id":31},"from-icolumn-class-to-typed-interface","From ",[14,34,16],{}," class to typed interface",[10,37,38],{},"Two years ago I had a class:",[40,41,46],"pre",{"className":42,"code":43,"language":44,"meta":45,"style":45},"language-javascript shiki shiki-themes github-light github-dark","export class IColumn {\n  constructor(options) {\n    const { title, rowKey, type = 'default', sortable = false, \u002F* ... *\u002F } = options;\n    this.title = title;\n    this.rowKey = rowKey;\n    this.sorter = sorter || ((a, b) => a[this.rowKey] > b[this.rowKey]);\n    this.headerComponent = this.processComponent(headerComponent);\n    this.rowComponent = this.processComponent(rowComponent);\n    \u002F\u002F ...\n  }\n\n  processComponent(component) {\n    if (!component) return undefined;\n    return {\n      is: component.is || '',\n      props: typeof component.props === 'function' ? component.props : () => component.props,\n      events: typeof component.events === 'function' ? component.events : () => component.events,\n    };\n  }\n}\n","javascript","",[14,47,48,56,62,68,74,80,86,92,98,104,110,117,123,129,135,141,147,153,159,164],{"__ignoreMap":45},[49,50,53],"span",{"class":51,"line":52},"line",1,[49,54,55],{},"export class IColumn {\n",[49,57,59],{"class":51,"line":58},2,[49,60,61],{},"  constructor(options) {\n",[49,63,65],{"class":51,"line":64},3,[49,66,67],{},"    const { title, rowKey, type = 'default', sortable = false, \u002F* ... *\u002F } = options;\n",[49,69,71],{"class":51,"line":70},4,[49,72,73],{},"    this.title = title;\n",[49,75,77],{"class":51,"line":76},5,[49,78,79],{},"    this.rowKey = rowKey;\n",[49,81,83],{"class":51,"line":82},6,[49,84,85],{},"    this.sorter = sorter || ((a, b) => a[this.rowKey] > b[this.rowKey]);\n",[49,87,89],{"class":51,"line":88},7,[49,90,91],{},"    this.headerComponent = this.processComponent(headerComponent);\n",[49,93,95],{"class":51,"line":94},8,[49,96,97],{},"    this.rowComponent = this.processComponent(rowComponent);\n",[49,99,101],{"class":51,"line":100},9,[49,102,103],{},"    \u002F\u002F ...\n",[49,105,107],{"class":51,"line":106},10,[49,108,109],{},"  }\n",[49,111,113],{"class":51,"line":112},11,[49,114,116],{"emptyLinePlaceholder":115},true,"\n",[49,118,120],{"class":51,"line":119},12,[49,121,122],{},"  processComponent(component) {\n",[49,124,126],{"class":51,"line":125},13,[49,127,128],{},"    if (!component) return undefined;\n",[49,130,132],{"class":51,"line":131},14,[49,133,134],{},"    return {\n",[49,136,138],{"class":51,"line":137},15,[49,139,140],{},"      is: component.is || '',\n",[49,142,144],{"class":51,"line":143},16,[49,145,146],{},"      props: typeof component.props === 'function' ? component.props : () => component.props,\n",[49,148,150],{"class":51,"line":149},17,[49,151,152],{},"      events: typeof component.events === 'function' ? component.events : () => component.events,\n",[49,154,156],{"class":51,"line":155},18,[49,157,158],{},"    };\n",[49,160,162],{"class":51,"line":161},19,[49,163,109],{},[49,165,167],{"class":51,"line":166},20,[49,168,169],{},"}\n",[10,171,172,173,176,177,180],{},"The class normalized props and events. If you passed a static object, it got wrapped in ",[14,174,175],{},"() => object"," so the table could always call it as a function. It was tidy on paper, and fine in JavaScript. The trouble started when TypeScript came in: generic row types didn't flow through a runtime class cleanly, and consumers lost type inference at exactly the spot where they were trying to reach into ",[14,178,179],{},"row.user.name",".",[10,182,183],{},"The current shape is a plain typed interface:",[40,185,189],{"className":186,"code":187,"language":188,"meta":45,"style":45},"language-typescript shiki shiki-themes github-light github-dark","export interface IDataTableColumn\u003CT = unknown> {\n  title: string | IDataTableRowFunction\u003CT>;\n  key: string | IDataTableRowFunction\u003CT>;\n  props?: Record\u003Cstring, unknown> | IDataTableRowFunction\u003CT>;\n  type?: 'default' | 'actions' | 'selection';\n  disabled?: boolean | IDataTableRowFunction\u003CT>;\n\n  headRender?: () => Array\u003CIDataTableVNodeChild> | IDataTableVNodeChild;\n  bodyRender?: IDataTableRenderFunction\u003CT>;\n\n  sortable?: boolean;\n  sorter?: (row1: T, row2: T) => number;\n  sortValue?: ISortValue;\n  extras?: Record\u003Cstring, unknown> | IDataTableRowFunction\u003CT>;\n}\n","typescript",[14,190,191,221,246,265,301,325,345,349,379,395,399,410,447,459,488],{"__ignoreMap":45},[49,192,193,197,200,204,208,211,214,218],{"class":51,"line":52},[49,194,196],{"class":195},"szBVR","export",[49,198,199],{"class":195}," interface",[49,201,203],{"class":202},"sScJk"," IDataTableColumn",[49,205,207],{"class":206},"sVt8B","\u003C",[49,209,210],{"class":202},"T",[49,212,213],{"class":195}," =",[49,215,217],{"class":216},"sj4cs"," unknown",[49,219,220],{"class":206},"> {\n",[49,222,223,227,230,233,236,239,241,243],{"class":51,"line":58},[49,224,226],{"class":225},"s4XuR","  title",[49,228,229],{"class":195},":",[49,231,232],{"class":216}," string",[49,234,235],{"class":195}," |",[49,237,238],{"class":202}," IDataTableRowFunction",[49,240,207],{"class":206},[49,242,210],{"class":202},[49,244,245],{"class":206},">;\n",[49,247,248,251,253,255,257,259,261,263],{"class":51,"line":64},[49,249,250],{"class":225},"  key",[49,252,229],{"class":195},[49,254,232],{"class":216},[49,256,235],{"class":195},[49,258,238],{"class":202},[49,260,207],{"class":206},[49,262,210],{"class":202},[49,264,245],{"class":206},[49,266,267,270,273,276,278,281,284,287,290,293,295,297,299],{"class":51,"line":70},[49,268,269],{"class":225},"  props",[49,271,272],{"class":195},"?:",[49,274,275],{"class":202}," Record",[49,277,207],{"class":206},[49,279,280],{"class":216},"string",[49,282,283],{"class":206},", ",[49,285,286],{"class":216},"unknown",[49,288,289],{"class":206},"> ",[49,291,292],{"class":195},"|",[49,294,238],{"class":202},[49,296,207],{"class":206},[49,298,210],{"class":202},[49,300,245],{"class":206},[49,302,303,306,308,312,314,317,319,322],{"class":51,"line":76},[49,304,305],{"class":225},"  type",[49,307,272],{"class":195},[49,309,311],{"class":310},"sZZnC"," 'default'",[49,313,235],{"class":195},[49,315,316],{"class":310}," 'actions'",[49,318,235],{"class":195},[49,320,321],{"class":310}," 'selection'",[49,323,324],{"class":206},";\n",[49,326,327,330,332,335,337,339,341,343],{"class":51,"line":82},[49,328,329],{"class":225},"  disabled",[49,331,272],{"class":195},[49,333,334],{"class":216}," boolean",[49,336,235],{"class":195},[49,338,238],{"class":202},[49,340,207],{"class":206},[49,342,210],{"class":202},[49,344,245],{"class":206},[49,346,347],{"class":51,"line":88},[49,348,116],{"emptyLinePlaceholder":115},[49,350,351,354,356,359,362,365,367,370,372,374,377],{"class":51,"line":94},[49,352,353],{"class":202},"  headRender",[49,355,272],{"class":195},[49,357,358],{"class":206}," () ",[49,360,361],{"class":195},"=>",[49,363,364],{"class":202}," Array",[49,366,207],{"class":206},[49,368,369],{"class":202},"IDataTableVNodeChild",[49,371,289],{"class":206},[49,373,292],{"class":195},[49,375,376],{"class":202}," IDataTableVNodeChild",[49,378,324],{"class":206},[49,380,381,384,386,389,391,393],{"class":51,"line":100},[49,382,383],{"class":225},"  bodyRender",[49,385,272],{"class":195},[49,387,388],{"class":202}," IDataTableRenderFunction",[49,390,207],{"class":206},[49,392,210],{"class":202},[49,394,245],{"class":206},[49,396,397],{"class":51,"line":106},[49,398,116],{"emptyLinePlaceholder":115},[49,400,401,404,406,408],{"class":51,"line":112},[49,402,403],{"class":225},"  sortable",[49,405,272],{"class":195},[49,407,334],{"class":216},[49,409,324],{"class":206},[49,411,412,415,417,420,423,425,428,430,433,435,437,440,442,445],{"class":51,"line":119},[49,413,414],{"class":202},"  sorter",[49,416,272],{"class":195},[49,418,419],{"class":206}," (",[49,421,422],{"class":225},"row1",[49,424,229],{"class":195},[49,426,427],{"class":202}," T",[49,429,283],{"class":206},[49,431,432],{"class":225},"row2",[49,434,229],{"class":195},[49,436,427],{"class":202},[49,438,439],{"class":206},") ",[49,441,361],{"class":195},[49,443,444],{"class":216}," number",[49,446,324],{"class":206},[49,448,449,452,454,457],{"class":51,"line":125},[49,450,451],{"class":225},"  sortValue",[49,453,272],{"class":195},[49,455,456],{"class":202}," ISortValue",[49,458,324],{"class":206},[49,460,461,464,466,468,470,472,474,476,478,480,482,484,486],{"class":51,"line":131},[49,462,463],{"class":225},"  extras",[49,465,272],{"class":195},[49,467,275],{"class":202},[49,469,207],{"class":206},[49,471,280],{"class":216},[49,473,283],{"class":206},[49,475,286],{"class":216},[49,477,289],{"class":206},[49,479,292],{"class":195},[49,481,238],{"class":202},[49,483,207],{"class":206},[49,485,210],{"class":202},[49,487,245],{"class":206},[49,489,490],{"class":51,"line":137},[49,491,169],{"class":206},[10,493,494],{},"Three things changed, and all three mattered.",[10,496,497,498,501,502,506],{},"First, no class. A column is just a plain object. TypeScript narrows it, editors autocomplete it, it serializes cleanly if you ever want to drive the table from JSON. The normalization the class used to do at construction time moved to a tiny helper in the renderer (",[14,499,500],{},"handleProps",", which we'll hit in a minute). The cost is one branch per cell render instead of one branch per column ",[503,504,505],"em",{},"definition",", and the benefit is the column stays a value, not a runtime object.",[10,508,509,510,512,513,516,517,520,521,524],{},"Second, the column is generic over ",[14,511,210],{},". ",[14,514,515],{},"IDataTableColumn\u003CUser>"," means ",[14,518,519],{},"key"," typed as ",[14,522,523],{},"string | (row: User) => ..."," which flows into every downstream callback. The day I added this, three call-sites at work silently picked up type errors for columns that had been quietly broken.",[10,526,527,528,531,532,535,536,539],{},"Third, render responsibilities split cleanly. ",[14,529,530],{},"headRender"," is for the header cell only; ",[14,533,534],{},"bodyRender"," for the body cell. The class version had a single ",[14,537,538],{},"rowComponent"," that did both and branched internally, which meant \"custom header, default body\" required an undocumented combination of nullish fields.",[10,541,542,543,546],{},"None of this is a new idea. The move from runtime classes to schema-shaped types is the story of most UI codebases in the last five years. The only reason it's worth mentioning is that I wrote the class version ",[503,544,545],{},"first"," and kept shipping it long after the typed version would have been cheaper. The cost of switching wasn't the refactor; it was admitting that the first design had run out.",[29,548,550,551,283,554,283,557],{"id":549},"the-render-trio-athead-atbody-atcell","The render trio: ",[14,552,553],{},"ATHead",[14,555,556],{},"ATBody",[14,558,559],{},"ATCell",[10,561,562,563,566,567,570,571,574,575,578],{},"The table component does not render rows directly. It renders a ",[14,564,565],{},"\u003Cthead>",", a ",[14,568,569],{},"\u003Ctbody>",", and those render ",[14,572,573],{},"\u003Ctr>"," elements that render ",[14,576,577],{},"\u003CATCell>"," components. The split is tight enough that each file has one job.",[10,580,581,584],{},[14,582,583],{},"ATHead.vue"," is the simplest:",[40,586,590],{"className":587,"code":588,"language":589,"meta":45,"style":45},"language-vue shiki shiki-themes github-light github-dark","\u003Ctemplate>\n  \u003Cthead>\n    \u003Ctr>\n      \u003CATCell\n        v-for=\"(column, collIndex) in props.columns\"\n        :key=\"collIndex\"\n        :binder=\"column.props\"\n        :cell=\"column\"\n        :head=\"true\"\n        :record=\"{} as S\"\n        :row-index=\"collIndex\"\n        @update-sorter=\"emit('updateSorter', collIndex)\"\n      \u002F>\n    \u003C\u002Ftr>\n  \u003C\u002Fthead>\n\u003C\u002Ftemplate>\n","vue",[14,591,592,603,613,623,631,642,652,662,672,682,692,701,711,716,725,734],{"__ignoreMap":45},[49,593,594,596,600],{"class":51,"line":52},[49,595,207],{"class":206},[49,597,599],{"class":598},"s9eBZ","template",[49,601,602],{"class":206},">\n",[49,604,605,608,611],{"class":51,"line":58},[49,606,607],{"class":206},"  \u003C",[49,609,610],{"class":598},"thead",[49,612,602],{"class":206},[49,614,615,618,621],{"class":51,"line":64},[49,616,617],{"class":206},"    \u003C",[49,619,620],{"class":598},"tr",[49,622,602],{"class":206},[49,624,625,628],{"class":51,"line":70},[49,626,627],{"class":206},"      \u003C",[49,629,630],{"class":598},"ATCell\n",[49,632,633,636,639],{"class":51,"line":76},[49,634,635],{"class":202},"        v-for",[49,637,638],{"class":206},"=",[49,640,641],{"class":310},"\"(column, collIndex) in props.columns\"\n",[49,643,644,647,649],{"class":51,"line":82},[49,645,646],{"class":202},"        :key",[49,648,638],{"class":206},[49,650,651],{"class":310},"\"collIndex\"\n",[49,653,654,657,659],{"class":51,"line":88},[49,655,656],{"class":202},"        :binder",[49,658,638],{"class":206},[49,660,661],{"class":310},"\"column.props\"\n",[49,663,664,667,669],{"class":51,"line":94},[49,665,666],{"class":202},"        :cell",[49,668,638],{"class":206},[49,670,671],{"class":310},"\"column\"\n",[49,673,674,677,679],{"class":51,"line":100},[49,675,676],{"class":202},"        :head",[49,678,638],{"class":206},[49,680,681],{"class":310},"\"true\"\n",[49,683,684,687,689],{"class":51,"line":106},[49,685,686],{"class":202},"        :record",[49,688,638],{"class":206},[49,690,691],{"class":310},"\"{} as S\"\n",[49,693,694,697,699],{"class":51,"line":112},[49,695,696],{"class":202},"        :row-index",[49,698,638],{"class":206},[49,700,651],{"class":310},[49,702,703,706,708],{"class":51,"line":119},[49,704,705],{"class":202},"        @update-sorter",[49,707,638],{"class":206},[49,709,710],{"class":310},"\"emit('updateSorter', collIndex)\"\n",[49,712,713],{"class":51,"line":125},[49,714,715],{"class":206},"      \u002F>\n",[49,717,718,721,723],{"class":51,"line":131},[49,719,720],{"class":206},"    \u003C\u002F",[49,722,620],{"class":598},[49,724,602],{"class":206},[49,726,727,730,732],{"class":51,"line":137},[49,728,729],{"class":206},"  \u003C\u002F",[49,731,610],{"class":598},[49,733,602],{"class":206},[49,735,736,739,741],{"class":51,"line":143},[49,737,738],{"class":206},"\u003C\u002F",[49,740,599],{"class":598},[49,742,602],{"class":206},[10,744,745,746,748,749,751,752,755,756,759,760,763,764,766,767,770,771,774],{},"One ",[14,747,573],{},". One ",[14,750,577],{}," per column, with ",[14,753,754],{},"head={true}",". The ",[14,757,758],{},"record"," is a synthetic empty object cast to ",[14,761,762],{},"S",". The header doesn't have a row, but ",[14,765,559],{}," is generic over it, so something has to fill the slot. Casting ",[14,768,769],{},"{} as S"," is the least-bad option; giving it an actual ",[14,772,773],{},"undefined"," would force every cell render to null-check.",[10,776,777,780],{},[14,778,779],{},"ATBody.vue"," is the same shape, one level up:",[40,782,784],{"className":587,"code":783,"language":589,"meta":45,"style":45},"\u003Ctbody>\n  \u003Ctr v-for=\"(dataItem, $index) in props.items\" :key=\"$index + '_dataItem'\">\n    \u003CATCell\n      v-for=\"(column, collIndex) in props.columns\"\n      :key=\"collIndex + '_dataItem_' + $index\"\n      :record=\"dataItem\"\n      :head=\"false\"\n      :row-index=\"$index\"\n      :cell=\"column\"\n      :binder=\"column.props\"\n    \u002F>\n  \u003C\u002Ftr>\n  \u003Cslot \u002F>\n\u003C\u002Ftbody>\n",[14,785,786,795,800,805,810,815,820,825,830,835,840,845,850,855],{"__ignoreMap":45},[49,787,788,790,793],{"class":51,"line":52},[49,789,207],{"class":206},[49,791,792],{"class":598},"tbody",[49,794,602],{"class":206},[49,796,797],{"class":51,"line":58},[49,798,799],{"class":206},"  \u003Ctr v-for=\"(dataItem, $index) in props.items\" :key=\"$index + '_dataItem'\">\n",[49,801,802],{"class":51,"line":64},[49,803,804],{"class":206},"    \u003CATCell\n",[49,806,807],{"class":51,"line":70},[49,808,809],{"class":206},"      v-for=\"(column, collIndex) in props.columns\"\n",[49,811,812],{"class":51,"line":76},[49,813,814],{"class":206},"      :key=\"collIndex + '_dataItem_' + $index\"\n",[49,816,817],{"class":51,"line":82},[49,818,819],{"class":206},"      :record=\"dataItem\"\n",[49,821,822],{"class":51,"line":88},[49,823,824],{"class":206},"      :head=\"false\"\n",[49,826,827],{"class":51,"line":94},[49,828,829],{"class":206},"      :row-index=\"$index\"\n",[49,831,832],{"class":51,"line":100},[49,833,834],{"class":206},"      :cell=\"column\"\n",[49,836,837],{"class":51,"line":106},[49,838,839],{"class":206},"      :binder=\"column.props\"\n",[49,841,842],{"class":51,"line":112},[49,843,844],{"class":206},"    \u002F>\n",[49,846,847],{"class":51,"line":119},[49,848,849],{"class":206},"  \u003C\u002Ftr>\n",[49,851,852],{"class":51,"line":125},[49,853,854],{"class":206},"  \u003Cslot \u002F>\n",[49,856,857,859,861],{"class":51,"line":131},[49,858,738],{"class":206},[49,860,792],{"class":598},[49,862,602],{"class":206},[10,864,865,866,283,869,872,873,876,877,880,881,884,885,888],{},"The composed keys (",[14,867,868],{},"$index + '_dataItem'",[14,870,871],{},"collIndex + '_dataItem_' + $index",") aren't paranoia. Vue's reconciler only cares about keys being unique ",[503,874,875],{},"within their parent list",", so the ",[14,878,879],{},"$index"," alone would be enough. But when I'm scrolling through Vue DevTools trying to figure out which row is which, having the key say \"",[14,882,883],{},"3_dataItem","\" instead of just \"",[14,886,887],{},"3","\" saves me a second. That's the only reason for the suffix.",[10,890,891,892,895,896,898,899,902,903,906,907,909],{},"The ",[14,893,894],{},"\u003Cslot \u002F>"," inside ",[14,897,569],{}," is where the no-data row lands. The outer component pipes ",[14,900,901],{},"\u003Ctr>\u003Ctd>...\u003C\u002Ftd>\u003C\u002Ftr>"," through it. Keeping the empty-state as a slotted row rather than a separate element means ",[14,904,905],{},"colspan"," math works against the same ",[14,908,573],{}," grid, and the border-collapsing styles don't break.",[10,911,912,913,180],{},"The real work happens in ",[14,914,915],{},"ATCell.vue",[29,917,919,921,922],{"id":918},"atcell-one-component-two-shapes-built-via-h",[14,920,559],{},": one component, two shapes, built via ",[14,923,924],{},"h()",[10,926,927,929,930,933,934,937,938,941],{},[14,928,559],{}," renders either a ",[14,931,932],{},"\u003Cth>"," or a ",[14,935,936],{},"\u003Ctd>",", depending on ",[14,939,940],{},"head",". Rather than writing two templates, it builds the vnode dynamically:",[40,943,945],{"className":186,"code":944,"language":188,"meta":45,"style":45},"const Cell = computed(() => {\n  return h(\n    props.head ? 'th' : 'td',\n    {\n      class: 'p-[16px] text-md gap-2 relative',\n      colspan: '1',\n      rowspan: '1',\n      ...handleProps\u003CT>(props.cell.props, props.record),\n      ...handleProps\u003CT>(props.binder, props.record),\n    },\n    h('div', { class: 'flex items-center gap-2 w-full' },\n      props.head\n        ? props.cell.sortable\n          ? [children(props.cell, props.record), sortButton(props.cell.sortValue?.applied)]\n          : children(props.cell, props.record)\n        : children(props.cell, props.record)\n    )\n  );\n});\n",[14,946,947,968,979,999,1004,1014,1024,1033,1047,1060,1065,1085,1090,1098,1118,1129,1138,1143,1148],{"__ignoreMap":45},[49,948,949,952,955,957,960,963,965],{"class":51,"line":52},[49,950,951],{"class":195},"const",[49,953,954],{"class":216}," Cell",[49,956,213],{"class":195},[49,958,959],{"class":202}," computed",[49,961,962],{"class":206},"(() ",[49,964,361],{"class":195},[49,966,967],{"class":206}," {\n",[49,969,970,973,976],{"class":51,"line":58},[49,971,972],{"class":195},"  return",[49,974,975],{"class":202}," h",[49,977,978],{"class":206},"(\n",[49,980,981,984,987,990,993,996],{"class":51,"line":64},[49,982,983],{"class":206},"    props.head ",[49,985,986],{"class":195},"?",[49,988,989],{"class":310}," 'th'",[49,991,992],{"class":195}," :",[49,994,995],{"class":310}," 'td'",[49,997,998],{"class":206},",\n",[49,1000,1001],{"class":51,"line":70},[49,1002,1003],{"class":206},"    {\n",[49,1005,1006,1009,1012],{"class":51,"line":76},[49,1007,1008],{"class":206},"      class: ",[49,1010,1011],{"class":310},"'p-[16px] text-md gap-2 relative'",[49,1013,998],{"class":206},[49,1015,1016,1019,1022],{"class":51,"line":82},[49,1017,1018],{"class":206},"      colspan: ",[49,1020,1021],{"class":310},"'1'",[49,1023,998],{"class":206},[49,1025,1026,1029,1031],{"class":51,"line":88},[49,1027,1028],{"class":206},"      rowspan: ",[49,1030,1021],{"class":310},[49,1032,998],{"class":206},[49,1034,1035,1038,1040,1042,1044],{"class":51,"line":94},[49,1036,1037],{"class":195},"      ...",[49,1039,500],{"class":202},[49,1041,207],{"class":206},[49,1043,210],{"class":202},[49,1045,1046],{"class":206},">(props.cell.props, props.record),\n",[49,1048,1049,1051,1053,1055,1057],{"class":51,"line":100},[49,1050,1037],{"class":195},[49,1052,500],{"class":202},[49,1054,207],{"class":206},[49,1056,210],{"class":202},[49,1058,1059],{"class":206},">(props.binder, props.record),\n",[49,1061,1062],{"class":51,"line":106},[49,1063,1064],{"class":206},"    },\n",[49,1066,1067,1070,1073,1076,1079,1082],{"class":51,"line":112},[49,1068,1069],{"class":202},"    h",[49,1071,1072],{"class":206},"(",[49,1074,1075],{"class":310},"'div'",[49,1077,1078],{"class":206},", { class: ",[49,1080,1081],{"class":310},"'flex items-center gap-2 w-full'",[49,1083,1084],{"class":206}," },\n",[49,1086,1087],{"class":51,"line":119},[49,1088,1089],{"class":206},"      props.head\n",[49,1091,1092,1095],{"class":51,"line":125},[49,1093,1094],{"class":195},"        ?",[49,1096,1097],{"class":206}," props.cell.sortable\n",[49,1099,1100,1103,1106,1109,1112,1115],{"class":51,"line":131},[49,1101,1102],{"class":195},"          ?",[49,1104,1105],{"class":206}," [",[49,1107,1108],{"class":202},"children",[49,1110,1111],{"class":206},"(props.cell, props.record), ",[49,1113,1114],{"class":202},"sortButton",[49,1116,1117],{"class":206},"(props.cell.sortValue?.applied)]\n",[49,1119,1120,1123,1126],{"class":51,"line":137},[49,1121,1122],{"class":195},"          :",[49,1124,1125],{"class":202}," children",[49,1127,1128],{"class":206},"(props.cell, props.record)\n",[49,1130,1131,1134,1136],{"class":51,"line":143},[49,1132,1133],{"class":195},"        :",[49,1135,1125],{"class":202},[49,1137,1128],{"class":206},[49,1139,1140],{"class":51,"line":149},[49,1141,1142],{"class":206},"    )\n",[49,1144,1145],{"class":51,"line":155},[49,1146,1147],{"class":206},"  );\n",[49,1149,1150],{"class":51,"line":161},[49,1151,1152],{"class":206},"});\n",[10,1154,1155],{},"Then the template just mounts the computed vnode:",[40,1157,1159],{"className":587,"code":1158,"language":589,"meta":45,"style":45},"\u003Ctemplate>\n  \u003Ccomponent :is=\"Cell\" :key=\"props.cell.sortValue?.applied || 'ATCell'\" \u002F>\n\u003C\u002Ftemplate>\n",[14,1160,1161,1169,1195],{"__ignoreMap":45},[49,1162,1163,1165,1167],{"class":51,"line":52},[49,1164,207],{"class":206},[49,1166,599],{"class":598},[49,1168,602],{"class":206},[49,1170,1171,1173,1176,1179,1181,1184,1187,1189,1192],{"class":51,"line":58},[49,1172,607],{"class":206},[49,1174,1175],{"class":598},"component",[49,1177,1178],{"class":202}," :is",[49,1180,638],{"class":206},[49,1182,1183],{"class":310},"\"Cell\"",[49,1185,1186],{"class":202}," :key",[49,1188,638],{"class":206},[49,1190,1191],{"class":310},"\"props.cell.sortValue?.applied || 'ATCell'\"",[49,1193,1194],{"class":206}," \u002F>\n",[49,1196,1197,1199,1201],{"class":51,"line":64},[49,1198,738],{"class":206},[49,1200,599],{"class":598},[49,1202,602],{"class":206},[10,1204,1205],{},"Two things are doing quiet work here.",[10,1207,1208,1210],{},[14,1209,500],{}," accepts either a plain object or a function of the row and always returns an object:",[40,1212,1214],{"className":186,"code":1213,"language":188,"meta":45,"style":45},"const handleProps = \u003CT,>(val: IDataTableColumn\u003CT>['props'], record: T) => {\n  if (!val) return {};\n  return typeof val === 'function' ? val(record) : val;\n};\n",[14,1215,1216,1265,1284,1314],{"__ignoreMap":45},[49,1217,1218,1220,1223,1225,1228,1230,1233,1236,1238,1240,1242,1244,1247,1250,1253,1255,1257,1259,1261,1263],{"class":51,"line":52},[49,1219,951],{"class":195},[49,1221,1222],{"class":202}," handleProps",[49,1224,213],{"class":195},[49,1226,1227],{"class":206}," \u003C",[49,1229,210],{"class":202},[49,1231,1232],{"class":206},",>(",[49,1234,1235],{"class":225},"val",[49,1237,229],{"class":195},[49,1239,203],{"class":202},[49,1241,207],{"class":206},[49,1243,210],{"class":202},[49,1245,1246],{"class":206},">[",[49,1248,1249],{"class":310},"'props'",[49,1251,1252],{"class":206},"], ",[49,1254,758],{"class":225},[49,1256,229],{"class":195},[49,1258,427],{"class":202},[49,1260,439],{"class":206},[49,1262,361],{"class":195},[49,1264,967],{"class":206},[49,1266,1267,1270,1272,1275,1278,1281],{"class":51,"line":58},[49,1268,1269],{"class":195},"  if",[49,1271,419],{"class":206},[49,1273,1274],{"class":195},"!",[49,1276,1277],{"class":206},"val) ",[49,1279,1280],{"class":195},"return",[49,1282,1283],{"class":206}," {};\n",[49,1285,1286,1288,1291,1294,1297,1300,1303,1306,1309,1311],{"class":51,"line":64},[49,1287,972],{"class":195},[49,1289,1290],{"class":195}," typeof",[49,1292,1293],{"class":206}," val ",[49,1295,1296],{"class":195},"===",[49,1298,1299],{"class":310}," 'function'",[49,1301,1302],{"class":195}," ?",[49,1304,1305],{"class":202}," val",[49,1307,1308],{"class":206},"(record) ",[49,1310,229],{"class":195},[49,1312,1313],{"class":206}," val;\n",[49,1315,1316],{"class":51,"line":70},[49,1317,1318],{"class":206},"};\n",[10,1320,1321,1322,1324],{},"That's the \"always callable\" contract the old ",[14,1323,16],{}," class enforced at construction, moved to the render path, where it's cheaper in the common case (static props don't get wrapped unnecessarily) and still gives consumers the choice.",[10,1326,1327,1328,1331,1332,1335,1336,1339,1340,1343,1344,1346],{},"The spread order matters: ",[14,1329,1330],{},"props.cell.props"," is spread first, ",[14,1333,1334],{},"props.binder"," second. ",[14,1337,1338],{},"binder"," is the same as ",[14,1341,1342],{},"cell.props","; it's passed in via a separate prop for historical reasons I'd collapse if I rewrote this. Until then, ",[14,1345,1338],{}," wins on conflict, which is what callers expect.",[1348,1349,891,1351,1354],"h3",{"id":1350},"the-keysortvalueapplied-that-looks-like-a-typo",[14,1352,1353],{},":key=\"sortValue?.applied\""," that looks like a typo",[10,1356,1357,1358,1361,1362,1365,1366,1369,1370,1372,1373,1376],{},"The second thing is that odd ",[14,1359,1360],{},":key=\"props.cell.sortValue?.applied || 'ATCell'\"",". The component's ",[14,1363,1364],{},"\u003Ccomponent :is>"," reacts to changes in the vnode, but the ",[14,1367,1368],{},":key"," looks like it's there to force a remount when the sort state flips. That's exactly what it's for. Vue's patching is smart enough to diff two ",[14,1371,924],{}," outputs, but if the cell's props came from a function of the record ",[503,1374,1375],{},"and"," the record changed at the same time the sort flipped, the patcher can sometimes hold onto a stale attribute. Keying by sort state forces a clean unmount\u002Fremount on exactly the transition where that can matter. It's one line. Removing it hasn't bitten me yet, which either means it's over-cautious or it's quietly preventing the bug. I'm not sure, and I've stopped trying to prove it either way. The cost of the key is a single render per sort toggle.",[29,1378,1380,1383],{"id":1379},"handlevaluebasedonkey-dot-paths-into-rows",[14,1381,1382],{},"handleValueBasedOnKey",": dot-paths into rows",[10,1385,1386,1387,1389,1390,1392],{},"When a column doesn't supply ",[14,1388,530],{}," or ",[14,1391,534],{},", the cell falls back to a default renderer:",[40,1394,1396],{"className":186,"code":1395,"language":188,"meta":45,"style":45},"const children = (cell: IDataTableColumn\u003CT>, record: T) => {\n  const defaultHeadCell = h('span', {\n    class: 'text-[16px] text-secondary font-medium capitalize',\n    innerHTML: typeof cell.title === 'function' ? cell.title(record) : cell.title,\n  });\n  const defaultBodyCell = h('span', {\n    class: 'text-[16px] text-foreground capitalize',\n    innerHTML: handleValueBasedOnKey(cell.key as string, record),\n  });\n  if (props.head) {\n    return cell.headRender ? cell.headRender() : defaultHeadCell;\n  }\n  return cell.bodyRender ? cell.bodyRender(record, props.rowIndex) : defaultBodyCell;\n};\n",[14,1397,1398,1434,1454,1464,1494,1499,1516,1525,1542,1546,1553,1575,1579,1600],{"__ignoreMap":45},[49,1399,1400,1402,1404,1406,1408,1411,1413,1415,1417,1419,1422,1424,1426,1428,1430,1432],{"class":51,"line":52},[49,1401,951],{"class":195},[49,1403,1125],{"class":202},[49,1405,213],{"class":195},[49,1407,419],{"class":206},[49,1409,1410],{"class":225},"cell",[49,1412,229],{"class":195},[49,1414,203],{"class":202},[49,1416,207],{"class":206},[49,1418,210],{"class":202},[49,1420,1421],{"class":206},">, ",[49,1423,758],{"class":225},[49,1425,229],{"class":195},[49,1427,427],{"class":202},[49,1429,439],{"class":206},[49,1431,361],{"class":195},[49,1433,967],{"class":206},[49,1435,1436,1439,1442,1444,1446,1448,1451],{"class":51,"line":58},[49,1437,1438],{"class":195},"  const",[49,1440,1441],{"class":216}," defaultHeadCell",[49,1443,213],{"class":195},[49,1445,975],{"class":202},[49,1447,1072],{"class":206},[49,1449,1450],{"class":310},"'span'",[49,1452,1453],{"class":206},", {\n",[49,1455,1456,1459,1462],{"class":51,"line":64},[49,1457,1458],{"class":206},"    class: ",[49,1460,1461],{"class":310},"'text-[16px] text-secondary font-medium capitalize'",[49,1463,998],{"class":206},[49,1465,1466,1469,1472,1475,1477,1479,1481,1484,1487,1489,1491],{"class":51,"line":70},[49,1467,1468],{"class":206},"    innerHTML: ",[49,1470,1471],{"class":195},"typeof",[49,1473,1474],{"class":206}," cell.title ",[49,1476,1296],{"class":195},[49,1478,1299],{"class":310},[49,1480,1302],{"class":195},[49,1482,1483],{"class":206}," cell.",[49,1485,1486],{"class":202},"title",[49,1488,1308],{"class":206},[49,1490,229],{"class":195},[49,1492,1493],{"class":206}," cell.title,\n",[49,1495,1496],{"class":51,"line":76},[49,1497,1498],{"class":206},"  });\n",[49,1500,1501,1503,1506,1508,1510,1512,1514],{"class":51,"line":82},[49,1502,1438],{"class":195},[49,1504,1505],{"class":216}," defaultBodyCell",[49,1507,213],{"class":195},[49,1509,975],{"class":202},[49,1511,1072],{"class":206},[49,1513,1450],{"class":310},[49,1515,1453],{"class":206},[49,1517,1518,1520,1523],{"class":51,"line":88},[49,1519,1458],{"class":206},[49,1521,1522],{"class":310},"'text-[16px] text-foreground capitalize'",[49,1524,998],{"class":206},[49,1526,1527,1529,1531,1534,1537,1539],{"class":51,"line":94},[49,1528,1468],{"class":206},[49,1530,1382],{"class":202},[49,1532,1533],{"class":206},"(cell.key ",[49,1535,1536],{"class":195},"as",[49,1538,232],{"class":216},[49,1540,1541],{"class":206},", record),\n",[49,1543,1544],{"class":51,"line":100},[49,1545,1498],{"class":206},[49,1547,1548,1550],{"class":51,"line":106},[49,1549,1269],{"class":195},[49,1551,1552],{"class":206}," (props.head) {\n",[49,1554,1555,1558,1561,1563,1565,1567,1570,1572],{"class":51,"line":112},[49,1556,1557],{"class":195},"    return",[49,1559,1560],{"class":206}," cell.headRender ",[49,1562,986],{"class":195},[49,1564,1483],{"class":206},[49,1566,530],{"class":202},[49,1568,1569],{"class":206},"() ",[49,1571,229],{"class":195},[49,1573,1574],{"class":206}," defaultHeadCell;\n",[49,1576,1577],{"class":51,"line":119},[49,1578,109],{"class":206},[49,1580,1581,1583,1586,1588,1590,1592,1595,1597],{"class":51,"line":125},[49,1582,972],{"class":195},[49,1584,1585],{"class":206}," cell.bodyRender ",[49,1587,986],{"class":195},[49,1589,1483],{"class":206},[49,1591,534],{"class":202},[49,1593,1594],{"class":206},"(record, props.rowIndex) ",[49,1596,229],{"class":195},[49,1598,1599],{"class":206}," defaultBodyCell;\n",[49,1601,1602],{"class":51,"line":131},[49,1603,1318],{"class":206},[10,1605,1606,1607,229],{},"The piece worth zooming in on is ",[14,1608,1382],{},[40,1610,1612],{"className":186,"code":1611,"language":188,"meta":45,"style":45},"const handleValueBasedOnKey = (key: string, receivedData: T): unknown => {\n  return key.split('.').reduce((acc: unknown, curr) => {\n    if (acc && typeof acc === 'object') {\n      return (acc as Record\u003Cstring, unknown>)[curr];\n    }\n    return undefined;\n  }, receivedData);\n};\n",[14,1613,1614,1652,1694,1718,1740,1745,1754,1759],{"__ignoreMap":45},[49,1615,1616,1618,1621,1623,1625,1627,1629,1631,1633,1636,1638,1640,1643,1645,1647,1650],{"class":51,"line":52},[49,1617,951],{"class":195},[49,1619,1620],{"class":202}," handleValueBasedOnKey",[49,1622,213],{"class":195},[49,1624,419],{"class":206},[49,1626,519],{"class":225},[49,1628,229],{"class":195},[49,1630,232],{"class":216},[49,1632,283],{"class":206},[49,1634,1635],{"class":225},"receivedData",[49,1637,229],{"class":195},[49,1639,427],{"class":202},[49,1641,1642],{"class":206},")",[49,1644,229],{"class":195},[49,1646,217],{"class":216},[49,1648,1649],{"class":195}," =>",[49,1651,967],{"class":206},[49,1653,1654,1656,1659,1662,1664,1667,1670,1673,1676,1679,1681,1683,1685,1688,1690,1692],{"class":51,"line":58},[49,1655,972],{"class":195},[49,1657,1658],{"class":206}," key.",[49,1660,1661],{"class":202},"split",[49,1663,1072],{"class":206},[49,1665,1666],{"class":310},"'.'",[49,1668,1669],{"class":206},").",[49,1671,1672],{"class":202},"reduce",[49,1674,1675],{"class":206},"((",[49,1677,1678],{"class":225},"acc",[49,1680,229],{"class":195},[49,1682,217],{"class":216},[49,1684,283],{"class":206},[49,1686,1687],{"class":225},"curr",[49,1689,439],{"class":206},[49,1691,361],{"class":195},[49,1693,967],{"class":206},[49,1695,1696,1699,1702,1705,1707,1710,1712,1715],{"class":51,"line":64},[49,1697,1698],{"class":195},"    if",[49,1700,1701],{"class":206}," (acc ",[49,1703,1704],{"class":195},"&&",[49,1706,1290],{"class":195},[49,1708,1709],{"class":206}," acc ",[49,1711,1296],{"class":195},[49,1713,1714],{"class":310}," 'object'",[49,1716,1717],{"class":206},") {\n",[49,1719,1720,1723,1725,1727,1729,1731,1733,1735,1737],{"class":51,"line":70},[49,1721,1722],{"class":195},"      return",[49,1724,1701],{"class":206},[49,1726,1536],{"class":195},[49,1728,275],{"class":202},[49,1730,207],{"class":206},[49,1732,280],{"class":216},[49,1734,283],{"class":206},[49,1736,286],{"class":216},[49,1738,1739],{"class":206},">)[curr];\n",[49,1741,1742],{"class":51,"line":76},[49,1743,1744],{"class":206},"    }\n",[49,1746,1747,1749,1752],{"class":51,"line":82},[49,1748,1557],{"class":195},[49,1750,1751],{"class":216}," undefined",[49,1753,324],{"class":206},[49,1755,1756],{"class":51,"line":88},[49,1757,1758],{"class":206},"  }, receivedData);\n",[49,1760,1761],{"class":51,"line":94},[49,1762,1318],{"class":206},[10,1764,1765,1766,1769,1770,1772,1773,1776],{},"A column with ",[14,1767,1768],{},"key: 'user.profile.displayName'"," walks the dots and pulls the nested value. This is the single feature that stops consumers from writing a ",[14,1771,534],{}," function 80% of the time. Most tables have three or four columns that are just ",[14,1774,1775],{},"row.foo.bar","; giving them a dot-path means the schema-as-data approach wins one more round.",[10,1778,1779,1780,1783,1784,1787,1788,1790,1791,1793],{},"I'm using ",[14,1781,1782],{},"innerHTML"," for the default renderer. That's deliberate for titles that want to include ",[14,1785,1786],{},"\u003Cbr>"," or a small bit of emphasis, and it's a footgun for user-controlled data. The convention I ended up with in code review is \"use the default for static titles and sanitized data; for anything user-generated, write a ",[14,1789,534],{}," that uses a child component.\" I haven't found a cleaner way to express that in the type system, and I'm not sure I want to force everyone through ",[14,1792,534],{}," for every cell.",[29,1795,1797],{"id":1796},"row-keys-the-fallback-when-nobody-supplies-one","Row keys: the fallback when nobody supplies one",[10,1799,1800,1801,1803,1804,1806,1807,1809],{},"The default Vue ",[14,1802,1368],{}," on rows is ",[14,1805,879],{},", which is the wrong answer for anything that changes order. To do better you need a stable identifier per row, and since the table is generic over ",[14,1808,210],{},", there's no field you can rely on universally.",[10,1811,1812],{},"The interface is the obvious one:",[40,1814,1816],{"className":186,"code":1815,"language":188,"meta":45,"style":45},"type DataTablePropsWithKey\u003CT> = IDataTableProps\u003CT> & {\n  \u002F** Optional stable key extractor; recommended *\u002F\n  rowKey?: (row: T) => string;\n};\n",[14,1817,1818,1848,1854,1878],{"__ignoreMap":45},[49,1819,1820,1823,1826,1828,1830,1832,1834,1837,1839,1841,1843,1846],{"class":51,"line":52},[49,1821,1822],{"class":195},"type",[49,1824,1825],{"class":202}," DataTablePropsWithKey",[49,1827,207],{"class":206},[49,1829,210],{"class":202},[49,1831,289],{"class":206},[49,1833,638],{"class":195},[49,1835,1836],{"class":202}," IDataTableProps",[49,1838,207],{"class":206},[49,1840,210],{"class":202},[49,1842,289],{"class":206},[49,1844,1845],{"class":195},"&",[49,1847,967],{"class":206},[49,1849,1850],{"class":51,"line":58},[49,1851,1853],{"class":1852},"sJ8bj","  \u002F** Optional stable key extractor; recommended *\u002F\n",[49,1855,1856,1859,1861,1863,1866,1868,1870,1872,1874,1876],{"class":51,"line":64},[49,1857,1858],{"class":202},"  rowKey",[49,1860,272],{"class":195},[49,1862,419],{"class":206},[49,1864,1865],{"class":225},"row",[49,1867,229],{"class":195},[49,1869,427],{"class":202},[49,1871,439],{"class":206},[49,1873,361],{"class":195},[49,1875,232],{"class":216},[49,1877,324],{"class":206},[49,1879,1880],{"class":51,"line":70},[49,1881,1318],{"class":206},[10,1883,1884],{},"In practice, nobody passes it. The fallback had to work for arbitrary rows:",[40,1886,1888],{"className":186,"code":1887,"language":188,"meta":45,"style":45},"function stableKeyFromObject(obj: Record\u003Cstring, unknown>): string {\n  if ('id' in obj && typeof obj.id !== 'undefined') {\n    return String(obj.id as unknown);\n  }\n  const keys = Object.keys(obj).sort();\n  return keys\n    .map((k) => `${k}:${String(obj[k])}`)\n    .join('|');\n}\n\nfunction getRowKey(row: T): string {\n  if (typeof globalProps.rowKey === 'function') return globalProps.rowKey(row);\n  return stableKeyFromObject(row as unknown as Record\u003Cstring, unknown>);\n}\n",[14,1889,1890,1924,1954,1971,1975,1999,2006,2052,2066,2070,2074,2097,2125,2154],{"__ignoreMap":45},[49,1891,1892,1895,1898,1900,1903,1905,1907,1909,1911,1913,1915,1918,1920,1922],{"class":51,"line":52},[49,1893,1894],{"class":195},"function",[49,1896,1897],{"class":202}," stableKeyFromObject",[49,1899,1072],{"class":206},[49,1901,1902],{"class":225},"obj",[49,1904,229],{"class":195},[49,1906,275],{"class":202},[49,1908,207],{"class":206},[49,1910,280],{"class":216},[49,1912,283],{"class":206},[49,1914,286],{"class":216},[49,1916,1917],{"class":206},">)",[49,1919,229],{"class":195},[49,1921,232],{"class":216},[49,1923,967],{"class":206},[49,1925,1926,1928,1930,1933,1936,1939,1941,1943,1946,1949,1952],{"class":51,"line":58},[49,1927,1269],{"class":195},[49,1929,419],{"class":206},[49,1931,1932],{"class":310},"'id'",[49,1934,1935],{"class":195}," in",[49,1937,1938],{"class":206}," obj ",[49,1940,1704],{"class":195},[49,1942,1290],{"class":195},[49,1944,1945],{"class":206}," obj.id ",[49,1947,1948],{"class":195},"!==",[49,1950,1951],{"class":310}," 'undefined'",[49,1953,1717],{"class":206},[49,1955,1956,1958,1961,1964,1966,1968],{"class":51,"line":64},[49,1957,1557],{"class":195},[49,1959,1960],{"class":202}," String",[49,1962,1963],{"class":206},"(obj.id ",[49,1965,1536],{"class":195},[49,1967,217],{"class":216},[49,1969,1970],{"class":206},");\n",[49,1972,1973],{"class":51,"line":70},[49,1974,109],{"class":206},[49,1976,1977,1979,1982,1984,1987,1990,1993,1996],{"class":51,"line":76},[49,1978,1438],{"class":195},[49,1980,1981],{"class":216}," keys",[49,1983,213],{"class":195},[49,1985,1986],{"class":206}," Object.",[49,1988,1989],{"class":202},"keys",[49,1991,1992],{"class":206},"(obj).",[49,1994,1995],{"class":202},"sort",[49,1997,1998],{"class":206},"();\n",[49,2000,2001,2003],{"class":51,"line":82},[49,2002,972],{"class":195},[49,2004,2005],{"class":206}," keys\n",[49,2007,2008,2011,2014,2016,2019,2021,2023,2026,2028,2031,2034,2036,2038,2041,2043,2046,2049],{"class":51,"line":88},[49,2009,2010],{"class":206},"    .",[49,2012,2013],{"class":202},"map",[49,2015,1675],{"class":206},[49,2017,2018],{"class":225},"k",[49,2020,439],{"class":206},[49,2022,361],{"class":195},[49,2024,2025],{"class":310}," `${",[49,2027,2018],{"class":206},[49,2029,2030],{"class":310},"}:${",[49,2032,2033],{"class":202},"String",[49,2035,1072],{"class":310},[49,2037,1902],{"class":206},[49,2039,2040],{"class":310},"[",[49,2042,2018],{"class":206},[49,2044,2045],{"class":310},"])",[49,2047,2048],{"class":310},"}`",[49,2050,2051],{"class":206},")\n",[49,2053,2054,2056,2059,2061,2064],{"class":51,"line":94},[49,2055,2010],{"class":206},[49,2057,2058],{"class":202},"join",[49,2060,1072],{"class":206},[49,2062,2063],{"class":310},"'|'",[49,2065,1970],{"class":206},[49,2067,2068],{"class":51,"line":100},[49,2069,169],{"class":206},[49,2071,2072],{"class":51,"line":106},[49,2073,116],{"emptyLinePlaceholder":115},[49,2075,2076,2078,2081,2083,2085,2087,2089,2091,2093,2095],{"class":51,"line":112},[49,2077,1894],{"class":195},[49,2079,2080],{"class":202}," getRowKey",[49,2082,1072],{"class":206},[49,2084,1865],{"class":225},[49,2086,229],{"class":195},[49,2088,427],{"class":202},[49,2090,1642],{"class":206},[49,2092,229],{"class":195},[49,2094,232],{"class":216},[49,2096,967],{"class":206},[49,2098,2099,2101,2103,2105,2108,2110,2112,2114,2116,2119,2122],{"class":51,"line":119},[49,2100,1269],{"class":195},[49,2102,419],{"class":206},[49,2104,1471],{"class":195},[49,2106,2107],{"class":206}," globalProps.rowKey ",[49,2109,1296],{"class":195},[49,2111,1299],{"class":310},[49,2113,439],{"class":206},[49,2115,1280],{"class":195},[49,2117,2118],{"class":206}," globalProps.",[49,2120,2121],{"class":202},"rowKey",[49,2123,2124],{"class":206},"(row);\n",[49,2126,2127,2129,2131,2134,2136,2138,2141,2143,2145,2147,2149,2151],{"class":51,"line":125},[49,2128,972],{"class":195},[49,2130,1897],{"class":202},[49,2132,2133],{"class":206},"(row ",[49,2135,1536],{"class":195},[49,2137,217],{"class":216},[49,2139,2140],{"class":195}," as",[49,2142,275],{"class":202},[49,2144,207],{"class":206},[49,2146,280],{"class":216},[49,2148,283],{"class":206},[49,2150,286],{"class":216},[49,2152,2153],{"class":206},">);\n",[49,2155,2156],{"class":51,"line":131},[49,2157,169],{"class":206},[10,2159,2160],{},"Three small calls, each earning its weight:",[2162,2163,2164,2179,2193],"ul",{},[2165,2166,2167,2174,2175,2178],"li",{},[2168,2169,2170,2173],"strong",{},[14,2171,2172],{},"id"," shortcut first."," Most backend records have one. ",[14,2176,2177],{},"String(obj.id)"," handles numbers, strings, and BigInts without branching. The cast is dishonest in the type system and honest at runtime.",[2165,2180,2181,2184,2185,2188,2189,2192],{},[2168,2182,2183],{},"Sort the keys before hashing."," ",[14,2186,2187],{},"Object.keys()"," returns insertion order. For most objects that order is the same every time, but \"most\" isn't \"all\". An object rebuilt from ",[14,2190,2191],{},"JSON.parse"," won't necessarily agree with one built from an object literal with the same keys. Sorting kills the whole class.",[2165,2194,2195,2184,2204,2206,2207,2209],{},[2168,2196,2197,2200,2201,180],{},[14,2198,2199],{},"String()"," over ",[14,2202,2203],{},"JSON.stringify",[14,2205,2199],{}," never throws; ",[14,2208,2203],{}," throws on circular references (which happen when you're careless with Pinia). The goal is a key that's stable, not a serializer.",[10,2211,2212,2213,2215,2216,2219,2220,2222,2223,2226],{},"The key is used both for Vue's reconciliation ",[503,2214,1375],{}," for selection. That's the invariant that matters. If the two used different notions of identity (",[14,2217,2218],{},":key=\"i\""," and selection using ",[14,2221,1296],{},"), a sort could drop checkboxes while keeping the data. Using ",[14,2224,2225],{},"getRowKey"," in both places means they can't drift.",[29,2228,2230,2231],{"id":2229},"selection-as-a-mapstring-t","Selection as a ",[14,2232,2233],{},"Map\u003Cstring, T>",[10,2235,2236,2237,2240,2241,2244],{},"There's no separate selection state. The component holds ",[14,2238,2239],{},"localSelectedItems"," (a ",[14,2242,2243],{},"shallowRef\u003CT[]>",") and derives a Map by key:",[40,2246,2248],{"className":186,"code":2247,"language":188,"meta":45,"style":45},"const hashedSelectedItems = computed(() => {\n  const map = new Map\u003Cstring, T>();\n  localSelectedItems.value.forEach((x) => map.set(getRowKey(x), x));\n  return map;\n});\n",[14,2249,2250,2267,2293,2323,2330],{"__ignoreMap":45},[49,2251,2252,2254,2257,2259,2261,2263,2265],{"class":51,"line":52},[49,2253,951],{"class":195},[49,2255,2256],{"class":216}," hashedSelectedItems",[49,2258,213],{"class":195},[49,2260,959],{"class":202},[49,2262,962],{"class":206},[49,2264,361],{"class":195},[49,2266,967],{"class":206},[49,2268,2269,2271,2274,2276,2279,2282,2284,2286,2288,2290],{"class":51,"line":58},[49,2270,1438],{"class":195},[49,2272,2273],{"class":216}," map",[49,2275,213],{"class":195},[49,2277,2278],{"class":195}," new",[49,2280,2281],{"class":202}," Map",[49,2283,207],{"class":206},[49,2285,280],{"class":216},[49,2287,283],{"class":206},[49,2289,210],{"class":202},[49,2291,2292],{"class":206},">();\n",[49,2294,2295,2298,2301,2303,2306,2308,2310,2313,2316,2318,2320],{"class":51,"line":64},[49,2296,2297],{"class":206},"  localSelectedItems.value.",[49,2299,2300],{"class":202},"forEach",[49,2302,1675],{"class":206},[49,2304,2305],{"class":225},"x",[49,2307,439],{"class":206},[49,2309,361],{"class":195},[49,2311,2312],{"class":206}," map.",[49,2314,2315],{"class":202},"set",[49,2317,1072],{"class":206},[49,2319,2225],{"class":202},[49,2321,2322],{"class":206},"(x), x));\n",[49,2324,2325,2327],{"class":51,"line":70},[49,2326,972],{"class":195},[49,2328,2329],{"class":206}," map;\n",[49,2331,2332],{"class":51,"line":76},[49,2333,1152],{"class":206},[10,2335,2336],{},"The selection column's header checkbox checks \"are all items selected\" with one array-scan:",[40,2338,2340],{"className":186,"code":2339,"language":188,"meta":45,"style":45},"const isAllDataSelected = computed(() => {\n  const atData = localItems.value.filter((item) =>\n    hashedSelectedItems.value.has(getRowKey(item))\n  );\n  return atData.length === localItems.value.length && localItems.value.length > 0;\n});\n",[14,2341,2342,2359,2384,2399,2403,2435],{"__ignoreMap":45},[49,2343,2344,2346,2349,2351,2353,2355,2357],{"class":51,"line":52},[49,2345,951],{"class":195},[49,2347,2348],{"class":216}," isAllDataSelected",[49,2350,213],{"class":195},[49,2352,959],{"class":202},[49,2354,962],{"class":206},[49,2356,361],{"class":195},[49,2358,967],{"class":206},[49,2360,2361,2363,2366,2368,2371,2374,2376,2379,2381],{"class":51,"line":58},[49,2362,1438],{"class":195},[49,2364,2365],{"class":216}," atData",[49,2367,213],{"class":195},[49,2369,2370],{"class":206}," localItems.value.",[49,2372,2373],{"class":202},"filter",[49,2375,1675],{"class":206},[49,2377,2378],{"class":225},"item",[49,2380,439],{"class":206},[49,2382,2383],{"class":195},"=>\n",[49,2385,2386,2389,2392,2394,2396],{"class":51,"line":64},[49,2387,2388],{"class":206},"    hashedSelectedItems.value.",[49,2390,2391],{"class":202},"has",[49,2393,1072],{"class":206},[49,2395,2225],{"class":202},[49,2397,2398],{"class":206},"(item))\n",[49,2400,2401],{"class":51,"line":70},[49,2402,1147],{"class":206},[49,2404,2405,2407,2410,2413,2416,2418,2420,2423,2425,2427,2430,2433],{"class":51,"line":76},[49,2406,972],{"class":195},[49,2408,2409],{"class":206}," atData.",[49,2411,2412],{"class":216},"length",[49,2414,2415],{"class":195}," ===",[49,2417,2370],{"class":206},[49,2419,2412],{"class":216},[49,2421,2422],{"class":195}," &&",[49,2424,2370],{"class":206},[49,2426,2412],{"class":216},[49,2428,2429],{"class":195}," >",[49,2431,2432],{"class":216}," 0",[49,2434,324],{"class":206},[49,2436,2437],{"class":51,"line":82},[49,2438,1152],{"class":206},[10,2440,2441],{},"O(n) across the visible items, which is already O(n) to render. Nothing we can do to improve that. But per-row selection checks are O(1):",[40,2443,2445],{"className":186,"code":2444,"language":188,"meta":45,"style":45},"modelValue: hashedSelectedItems.value.has(getRowKey(rowData)),\n",[14,2446,2447],{"__ignoreMap":45},[49,2448,2449,2452,2455,2457,2459,2461],{"class":51,"line":52},[49,2450,2451],{"class":202},"modelValue",[49,2453,2454],{"class":206},": hashedSelectedItems.value.",[49,2456,2391],{"class":202},[49,2458,1072],{"class":206},[49,2460,2225],{"class":202},[49,2462,2463],{"class":206},"(rowData)),\n",[10,2465,2466,2467,2470],{},"Without the computed Map, each checkbox would do ",[14,2468,2469],{},"localSelectedItems.value.find(...)"," on render. A 100-row table with 100 selected items would be doing 10,000 comparisons per re-render. The Map turns that into 100 constant-time lookups. You only notice this once you've blown up a table with real data. At which point you learn the pattern and stop writing the naïve version.",[10,2472,2473,2474,2477],{},"The selection column itself is built on demand. The consumer either declares it as a column (",[14,2475,2476],{},"type: 'selection'",") or doesn't:",[40,2479,2481],{"className":186,"code":2480,"language":188,"meta":45,"style":45},"if (hasSelectColumn) {\n  const selectionColumn = handleSelectionColumn();\n  _columns = _columns.filter((col) => col.type !== 'selection');\n  _columns.unshift(selectionColumn);\n}\n",[14,2482,2483,2491,2505,2535,2546],{"__ignoreMap":45},[49,2484,2485,2488],{"class":51,"line":52},[49,2486,2487],{"class":195},"if",[49,2489,2490],{"class":206}," (hasSelectColumn) {\n",[49,2492,2493,2495,2498,2500,2503],{"class":51,"line":58},[49,2494,1438],{"class":195},[49,2496,2497],{"class":216}," selectionColumn",[49,2499,213],{"class":195},[49,2501,2502],{"class":202}," handleSelectionColumn",[49,2504,1998],{"class":206},[49,2506,2507,2510,2512,2515,2517,2519,2522,2524,2526,2529,2531,2533],{"class":51,"line":64},[49,2508,2509],{"class":206},"  _columns ",[49,2511,638],{"class":195},[49,2513,2514],{"class":206}," _columns.",[49,2516,2373],{"class":202},[49,2518,1675],{"class":206},[49,2520,2521],{"class":225},"col",[49,2523,439],{"class":206},[49,2525,361],{"class":195},[49,2527,2528],{"class":206}," col.type ",[49,2530,1948],{"class":195},[49,2532,321],{"class":310},[49,2534,1970],{"class":206},[49,2536,2537,2540,2543],{"class":51,"line":70},[49,2538,2539],{"class":206},"  _columns.",[49,2541,2542],{"class":202},"unshift",[49,2544,2545],{"class":206},"(selectionColumn);\n",[49,2547,2548],{"class":51,"line":76},[49,2549,169],{"class":206},[10,2551,2552,2553,2555,2556,2559,2560,2563],{},"The existing ",[14,2554,2476],{}," column, if any, is replaced with the internally-built one and forced to position 0. The consumer's instance is a ",[503,2557,2558],{},"marker"," (\"I want selection in this table\"), and the component writes the actual renderers. This gives consumers the API surface of \"selection is a column\" without requiring them to know how to build checkboxes and wire up the ",[14,2561,2562],{},"indeterminate"," state.",[29,2565,2567,2568,2571],{"id":2566},"actions-and-the-markraw-that-earned-its-keep","Actions, and the ",[14,2569,2570],{},"markRaw"," that earned its keep",[10,2573,2574,2575,2578],{},"Actions work the same way: the consumer declares an actions column (or the component auto-appends one if ",[14,2576,2577],{},"actions.length > 0","), and the renderer is built internally:",[40,2580,2582],{"className":186,"code":2581,"language":188,"meta":45,"style":45},"const actionsComponent: IDataTableRenderFunction\u003CT> = (rowData, index) =>\n  h(markRaw(AActionGroup), {\n    actions: localActions.value as IAction\u003Cunknown>[],\n    record: rowData,\n    popover: globalProps.popover,\n    maxMainCount: getVisibleLength(rowData),\n    onUpdateAction: (action) =>\n      emit('updateAction', { action, row: rowData, rowIndex: index }),\n  });\n",[14,2583,2584,2617,2629,2646,2651,2656,2667,2682,2695],{"__ignoreMap":45},[49,2585,2586,2588,2591,2593,2595,2597,2599,2601,2603,2605,2608,2610,2613,2615],{"class":51,"line":52},[49,2587,951],{"class":195},[49,2589,2590],{"class":202}," actionsComponent",[49,2592,229],{"class":195},[49,2594,388],{"class":202},[49,2596,207],{"class":206},[49,2598,210],{"class":202},[49,2600,289],{"class":206},[49,2602,638],{"class":195},[49,2604,419],{"class":206},[49,2606,2607],{"class":225},"rowData",[49,2609,283],{"class":206},[49,2611,2612],{"class":225},"index",[49,2614,439],{"class":206},[49,2616,2383],{"class":195},[49,2618,2619,2622,2624,2626],{"class":51,"line":58},[49,2620,2621],{"class":202},"  h",[49,2623,1072],{"class":206},[49,2625,2570],{"class":202},[49,2627,2628],{"class":206},"(AActionGroup), {\n",[49,2630,2631,2634,2636,2639,2641,2643],{"class":51,"line":64},[49,2632,2633],{"class":206},"    actions: localActions.value ",[49,2635,1536],{"class":195},[49,2637,2638],{"class":202}," IAction",[49,2640,207],{"class":206},[49,2642,286],{"class":216},[49,2644,2645],{"class":206},">[],\n",[49,2647,2648],{"class":51,"line":70},[49,2649,2650],{"class":206},"    record: rowData,\n",[49,2652,2653],{"class":51,"line":76},[49,2654,2655],{"class":206},"    popover: globalProps.popover,\n",[49,2657,2658,2661,2664],{"class":51,"line":82},[49,2659,2660],{"class":206},"    maxMainCount: ",[49,2662,2663],{"class":202},"getVisibleLength",[49,2665,2666],{"class":206},"(rowData),\n",[49,2668,2669,2672,2675,2678,2680],{"class":51,"line":88},[49,2670,2671],{"class":202},"    onUpdateAction",[49,2673,2674],{"class":206},": (",[49,2676,2677],{"class":225},"action",[49,2679,439],{"class":206},[49,2681,2383],{"class":195},[49,2683,2684,2687,2689,2692],{"class":51,"line":94},[49,2685,2686],{"class":202},"      emit",[49,2688,1072],{"class":206},[49,2690,2691],{"class":310},"'updateAction'",[49,2693,2694],{"class":206},", { action, row: rowData, rowIndex: index }),\n",[49,2696,2697],{"class":51,"line":100},[49,2698,1498],{"class":206},[10,2700,2701,2704],{},[14,2702,2703],{},"markRaw(AActionGroup)"," is the line I want to isolate.",[10,2706,2707,2708,2710,2711,2713,2714,566,2717,2720],{},"When you pass a component definition to ",[14,2709,924],{},", Vue touches it during render. Without ",[14,2712,2570],{},", if that component definition ever passes through Vue's reactivity system (a ",[14,2715,2716],{},"ref",[14,2718,2719],{},"reactive",", a prop) it gets wrapped in a Proxy. Component definitions aren't meant to be reactive. They're static metadata. Wrapping them doesn't break anything; it just makes every property read go through a handler that does a reactivity-tracking check that never pays off.",[10,2722,2723,2724,2727,2728,2730,2731,2734],{},"The render function above runs once per row. For a table with 500 rows, the first render touches ",[14,2725,2726],{},"AActionGroup","'s definition 500 times. ",[14,2729,2570],{}," sets a ",[14,2732,2733],{},"__v_skip"," flag on the object once, and every subsequent render skips the Proxy path.",[10,2736,2737,2738,2740,2741,2744,2745,2747],{},"The cost of ",[14,2739,2570],{}," is permanent. You can't un-mark it. That's fine for a component definition: it's a static import. If you wanted to hot-swap the action component at runtime, you'd do that at a different layer (picking ",[503,2742,2743],{},"which"," component you pass to ",[14,2746,2570],{},"), not by making the reference reactive.",[10,2749,2750,2751,2753,2754,2756,2757,2760,2761,2763],{},"There's also a dev-mode warning (\"Vue received a Component that was made a reactive object\") that fires when a reactive component slips into ",[14,2752,924],{},". I hit that on a 1200-row table where ",[14,2755,2726],{}," was passed through a ",[14,2758,2759],{},"shallowRef"," for theming. Adding ",[14,2762,2570],{}," silenced it and dropped the first-paint time noticeably.",[10,2765,2766],{},"The visible-action count can be either a number or a function of the row:",[40,2768,2770],{"className":186,"code":2769,"language":188,"meta":45,"style":45},"const getVisibleLength = (rowData: T) =>\n  typeof globalProps.visibleActionLength === 'function'\n    ? globalProps.visibleActionLength(rowData)\n    : globalProps.visibleActionLength;\n",[14,2771,2772,2793,2806,2819],{"__ignoreMap":45},[49,2773,2774,2776,2779,2781,2783,2785,2787,2789,2791],{"class":51,"line":52},[49,2775,951],{"class":195},[49,2777,2778],{"class":202}," getVisibleLength",[49,2780,213],{"class":195},[49,2782,419],{"class":206},[49,2784,2607],{"class":225},[49,2786,229],{"class":195},[49,2788,427],{"class":202},[49,2790,439],{"class":206},[49,2792,2383],{"class":195},[49,2794,2795,2798,2801,2803],{"class":51,"line":58},[49,2796,2797],{"class":195},"  typeof",[49,2799,2800],{"class":206}," globalProps.visibleActionLength ",[49,2802,1296],{"class":195},[49,2804,2805],{"class":310}," 'function'\n",[49,2807,2808,2811,2813,2816],{"class":51,"line":64},[49,2809,2810],{"class":195},"    ?",[49,2812,2118],{"class":206},[49,2814,2815],{"class":202},"visibleActionLength",[49,2817,2818],{"class":206},"(rowData)\n",[49,2820,2821,2824],{"class":51,"line":70},[49,2822,2823],{"class":195},"    :",[49,2825,2826],{"class":206}," globalProps.visibleActionLength;\n",[10,2828,2829],{},"Which is the only way to do \"show 3 actions for admins, 1 for regular users\" without building a second actions column.",[29,2831,2833],{"id":2832},"sorting-a-three-click-cycle-local-or-remote","Sorting: a three-click cycle, local or remote",[10,2835,2836],{},"Sort state is a two-boolean object:",[40,2838,2840],{"className":186,"code":2839,"language":188,"meta":45,"style":45},"export type ISortValue = { applied: boolean; revert: boolean };\n",[14,2841,2842],{"__ignoreMap":45},[49,2843,2844,2846,2849,2851,2853,2856,2859,2861,2863,2866,2869,2871,2873],{"class":51,"line":52},[49,2845,196],{"class":195},[49,2847,2848],{"class":195}," type",[49,2850,456],{"class":202},[49,2852,213],{"class":195},[49,2854,2855],{"class":206}," { ",[49,2857,2858],{"class":225},"applied",[49,2860,229],{"class":195},[49,2862,334],{"class":216},[49,2864,2865],{"class":206},"; ",[49,2867,2868],{"class":225},"revert",[49,2870,229],{"class":195},[49,2872,334],{"class":216},[49,2874,2875],{"class":206}," };\n",[10,2877,2878,2879,2882],{},"Not ",[14,2880,2881],{},"'asc' | 'desc' | null",". I tried strings first; they kept losing the \"unapplied\" state because falsy values of the wrong type don't coexist gracefully with strict TypeScript.",[10,2884,2885],{},"The toggle cycles through three states:",[40,2887,2889],{"className":186,"code":2888,"language":188,"meta":45,"style":45},"function handleToggleSortButton(sorterValue: ISortValue): ISortValue {\n  if (!sorterValue.applied) {\n    sorterValue.applied = true;\n  } else if (!sorterValue.revert) {\n    sorterValue.revert = true;\n  } else {\n    sorterValue.applied = false;\n    sorterValue.revert = false;\n  }\n  return sorterValue;\n}\n",[14,2890,2891,2915,2926,2938,2956,2967,2975,2986,2996,3000,3007],{"__ignoreMap":45},[49,2892,2893,2895,2898,2900,2903,2905,2907,2909,2911,2913],{"class":51,"line":52},[49,2894,1894],{"class":195},[49,2896,2897],{"class":202}," handleToggleSortButton",[49,2899,1072],{"class":206},[49,2901,2902],{"class":225},"sorterValue",[49,2904,229],{"class":195},[49,2906,456],{"class":202},[49,2908,1642],{"class":206},[49,2910,229],{"class":195},[49,2912,456],{"class":202},[49,2914,967],{"class":206},[49,2916,2917,2919,2921,2923],{"class":51,"line":58},[49,2918,1269],{"class":195},[49,2920,419],{"class":206},[49,2922,1274],{"class":195},[49,2924,2925],{"class":206},"sorterValue.applied) {\n",[49,2927,2928,2931,2933,2936],{"class":51,"line":64},[49,2929,2930],{"class":206},"    sorterValue.applied ",[49,2932,638],{"class":195},[49,2934,2935],{"class":216}," true",[49,2937,324],{"class":206},[49,2939,2940,2943,2946,2949,2951,2953],{"class":51,"line":70},[49,2941,2942],{"class":206},"  } ",[49,2944,2945],{"class":195},"else",[49,2947,2948],{"class":195}," if",[49,2950,419],{"class":206},[49,2952,1274],{"class":195},[49,2954,2955],{"class":206},"sorterValue.revert) {\n",[49,2957,2958,2961,2963,2965],{"class":51,"line":76},[49,2959,2960],{"class":206},"    sorterValue.revert ",[49,2962,638],{"class":195},[49,2964,2935],{"class":216},[49,2966,324],{"class":206},[49,2968,2969,2971,2973],{"class":51,"line":82},[49,2970,2942],{"class":206},[49,2972,2945],{"class":195},[49,2974,967],{"class":206},[49,2976,2977,2979,2981,2984],{"class":51,"line":88},[49,2978,2930],{"class":206},[49,2980,638],{"class":195},[49,2982,2983],{"class":216}," false",[49,2985,324],{"class":206},[49,2987,2988,2990,2992,2994],{"class":51,"line":94},[49,2989,2960],{"class":206},[49,2991,638],{"class":195},[49,2993,2983],{"class":216},[49,2995,324],{"class":206},[49,2997,2998],{"class":51,"line":100},[49,2999,109],{"class":206},[49,3001,3002,3004],{"class":51,"line":106},[49,3003,972],{"class":195},[49,3005,3006],{"class":206}," sorterValue;\n",[49,3008,3009],{"class":51,"line":112},[49,3010,169],{"class":206},[10,3012,3013,3014,3017],{},"Click once: ascending. Click again: descending. Click a third time: back to the original order. Users expect this on every table they've ever used, and they're always surprised when a table ",[503,3015,3016],{},"doesn't"," have the \"unsort\" click.",[10,3019,3020],{},"The actual sorting has two paths: remote and local.",[40,3022,3024],{"className":186,"code":3023,"language":188,"meta":45,"style":45},"function sortCell(columnIndex: number) {\n  const column = localColumns.value[columnIndex]!;\n  const sorterFunction = column.sorter;\n  const sorterValue = handleToggleSortButton(column.sortValue!);\n\n  if (globalProps.remote) {\n    emit('updateSorter', { column, sorterValue, rowIndex: columnIndex });\n    return;\n  }\n\n  if (!sorterValue.applied && !sorterValue.revert) {\n    localItems.value = _.cloneDeep(globalProps.items);\n    emit('updateSorter', { column, sorterValue, rowIndex: columnIndex });\n    return;\n  }\n\n  localItems.value = [...localItems.value].sort((a, b) => {\n    const result = sorterFunction!(a, b);\n    return sorterValue.revert ? -result : result;\n  });\n  emit('updateSorter', { column, sorterValue, rowIndex: columnIndex });\n}\n",[14,3025,3026,3044,3060,3072,3090,3094,3101,3114,3120,3124,3128,3146,3162,3172,3178,3182,3186,3219,3236,3256,3260,3272],{"__ignoreMap":45},[49,3027,3028,3030,3033,3035,3038,3040,3042],{"class":51,"line":52},[49,3029,1894],{"class":195},[49,3031,3032],{"class":202}," sortCell",[49,3034,1072],{"class":206},[49,3036,3037],{"class":225},"columnIndex",[49,3039,229],{"class":195},[49,3041,444],{"class":216},[49,3043,1717],{"class":206},[49,3045,3046,3048,3051,3053,3056,3058],{"class":51,"line":58},[49,3047,1438],{"class":195},[49,3049,3050],{"class":216}," column",[49,3052,213],{"class":195},[49,3054,3055],{"class":206}," localColumns.value[columnIndex]",[49,3057,1274],{"class":195},[49,3059,324],{"class":206},[49,3061,3062,3064,3067,3069],{"class":51,"line":64},[49,3063,1438],{"class":195},[49,3065,3066],{"class":216}," sorterFunction",[49,3068,213],{"class":195},[49,3070,3071],{"class":206}," column.sorter;\n",[49,3073,3074,3076,3079,3081,3083,3086,3088],{"class":51,"line":70},[49,3075,1438],{"class":195},[49,3077,3078],{"class":216}," sorterValue",[49,3080,213],{"class":195},[49,3082,2897],{"class":202},[49,3084,3085],{"class":206},"(column.sortValue",[49,3087,1274],{"class":195},[49,3089,1970],{"class":206},[49,3091,3092],{"class":51,"line":76},[49,3093,116],{"emptyLinePlaceholder":115},[49,3095,3096,3098],{"class":51,"line":82},[49,3097,1269],{"class":195},[49,3099,3100],{"class":206}," (globalProps.remote) {\n",[49,3102,3103,3106,3108,3111],{"class":51,"line":88},[49,3104,3105],{"class":202},"    emit",[49,3107,1072],{"class":206},[49,3109,3110],{"class":310},"'updateSorter'",[49,3112,3113],{"class":206},", { column, sorterValue, rowIndex: columnIndex });\n",[49,3115,3116,3118],{"class":51,"line":94},[49,3117,1557],{"class":195},[49,3119,324],{"class":206},[49,3121,3122],{"class":51,"line":100},[49,3123,109],{"class":206},[49,3125,3126],{"class":51,"line":106},[49,3127,116],{"emptyLinePlaceholder":115},[49,3129,3130,3132,3134,3136,3139,3141,3144],{"class":51,"line":112},[49,3131,1269],{"class":195},[49,3133,419],{"class":206},[49,3135,1274],{"class":195},[49,3137,3138],{"class":206},"sorterValue.applied ",[49,3140,1704],{"class":195},[49,3142,3143],{"class":195}," !",[49,3145,2955],{"class":206},[49,3147,3148,3151,3153,3156,3159],{"class":51,"line":119},[49,3149,3150],{"class":206},"    localItems.value ",[49,3152,638],{"class":195},[49,3154,3155],{"class":206}," _.",[49,3157,3158],{"class":202},"cloneDeep",[49,3160,3161],{"class":206},"(globalProps.items);\n",[49,3163,3164,3166,3168,3170],{"class":51,"line":125},[49,3165,3105],{"class":202},[49,3167,1072],{"class":206},[49,3169,3110],{"class":310},[49,3171,3113],{"class":206},[49,3173,3174,3176],{"class":51,"line":131},[49,3175,1557],{"class":195},[49,3177,324],{"class":206},[49,3179,3180],{"class":51,"line":137},[49,3181,109],{"class":206},[49,3183,3184],{"class":51,"line":143},[49,3185,116],{"emptyLinePlaceholder":115},[49,3187,3188,3191,3193,3195,3198,3201,3203,3205,3208,3210,3213,3215,3217],{"class":51,"line":149},[49,3189,3190],{"class":206},"  localItems.value ",[49,3192,638],{"class":195},[49,3194,1105],{"class":206},[49,3196,3197],{"class":195},"...",[49,3199,3200],{"class":206},"localItems.value].",[49,3202,1995],{"class":202},[49,3204,1675],{"class":206},[49,3206,3207],{"class":225},"a",[49,3209,283],{"class":206},[49,3211,3212],{"class":225},"b",[49,3214,439],{"class":206},[49,3216,361],{"class":195},[49,3218,967],{"class":206},[49,3220,3221,3224,3227,3229,3231,3233],{"class":51,"line":155},[49,3222,3223],{"class":195},"    const",[49,3225,3226],{"class":216}," result",[49,3228,213],{"class":195},[49,3230,3066],{"class":202},[49,3232,1274],{"class":195},[49,3234,3235],{"class":206},"(a, b);\n",[49,3237,3238,3240,3243,3245,3248,3251,3253],{"class":51,"line":161},[49,3239,1557],{"class":195},[49,3241,3242],{"class":206}," sorterValue.revert ",[49,3244,986],{"class":195},[49,3246,3247],{"class":195}," -",[49,3249,3250],{"class":206},"result ",[49,3252,229],{"class":195},[49,3254,3255],{"class":206}," result;\n",[49,3257,3258],{"class":51,"line":166},[49,3259,1498],{"class":206},[49,3261,3263,3266,3268,3270],{"class":51,"line":3262},21,[49,3264,3265],{"class":202},"  emit",[49,3267,1072],{"class":206},[49,3269,3110],{"class":310},[49,3271,3113],{"class":206},[49,3273,3275],{"class":51,"line":3274},22,[49,3276,169],{"class":206},[10,3278,3279,3280,3283],{},"Remote mode emits and lets the parent handle pagination + sorting against the backend. Local mode runs the sorter in-memory, with one quirk: when the user returns to the \"unsorted\" state, the local items get re-cloned from ",[14,3281,3282],{},"globalProps.items",". That restores original order without needing to remember what it was.",[10,3285,3286],{},"The emit fires in all three cases. Remote backends need it to refetch; local consumers can use it to persist the user's sort choice to a URL query param. Emitting unconditionally is cheaper than branching, and consumers can ignore it.",[29,3288,3290,3291,283,3293,283,3295],{"id":3289},"reactivity-shallowref-clonedeep-debouncedwatch","Reactivity: ",[14,3292,2759],{},[14,3294,3158],{},[14,3296,3297],{},"debouncedWatch",[10,3299,3300,3301,283,3304,283,3307,3310,3311,3313,3314,229],{},"The component does not deep-watch the consumer's data. ",[14,3302,3303],{},"localItems",[14,3305,3306],{},"localColumns",[14,3308,3309],{},"localActions",", and ",[14,3312,2239],{}," are all ",[14,3315,2759],{},[40,3317,3319],{"className":186,"code":3318,"language":188,"meta":45,"style":45},"const localItems = shallowRef\u003CT[]>(_.cloneDeep(globalProps.items));\nconst localColumns = shallowRef\u003CIDataTableColumn\u003CT>[]>([]);\nconst localActions = shallowRef\u003CIAction\u003CT>[]>(_.cloneDeep(globalProps.actions));\nconst localSelectedItems = shallowRef\u003CT[]>(_.cloneDeep(globalProps.selectedItems));\n",[14,3320,3321,3345,3368,3396],{"__ignoreMap":45},[49,3322,3323,3325,3328,3330,3333,3335,3337,3340,3342],{"class":51,"line":52},[49,3324,951],{"class":195},[49,3326,3327],{"class":216}," localItems",[49,3329,213],{"class":195},[49,3331,3332],{"class":202}," shallowRef",[49,3334,207],{"class":206},[49,3336,210],{"class":202},[49,3338,3339],{"class":206},"[]>(_.",[49,3341,3158],{"class":202},[49,3343,3344],{"class":206},"(globalProps.items));\n",[49,3346,3347,3349,3352,3354,3356,3358,3361,3363,3365],{"class":51,"line":58},[49,3348,951],{"class":195},[49,3350,3351],{"class":216}," localColumns",[49,3353,213],{"class":195},[49,3355,3332],{"class":202},[49,3357,207],{"class":206},[49,3359,3360],{"class":202},"IDataTableColumn",[49,3362,207],{"class":206},[49,3364,210],{"class":202},[49,3366,3367],{"class":206},">[]>([]);\n",[49,3369,3370,3372,3375,3377,3379,3381,3384,3386,3388,3391,3393],{"class":51,"line":64},[49,3371,951],{"class":195},[49,3373,3374],{"class":216}," localActions",[49,3376,213],{"class":195},[49,3378,3332],{"class":202},[49,3380,207],{"class":206},[49,3382,3383],{"class":202},"IAction",[49,3385,207],{"class":206},[49,3387,210],{"class":202},[49,3389,3390],{"class":206},">[]>(_.",[49,3392,3158],{"class":202},[49,3394,3395],{"class":206},"(globalProps.actions));\n",[49,3397,3398,3400,3403,3405,3407,3409,3411,3413,3415],{"class":51,"line":70},[49,3399,951],{"class":195},[49,3401,3402],{"class":216}," localSelectedItems",[49,3404,213],{"class":195},[49,3406,3332],{"class":202},[49,3408,207],{"class":206},[49,3410,210],{"class":202},[49,3412,3339],{"class":206},[49,3414,3158],{"class":202},[49,3416,3417],{"class":206},"(globalProps.selectedItems));\n",[10,3419,3420,3421,3423,3424,3427,3428,3430],{},"A ",[14,3422,2716],{}," wraps arrays in a reactive proxy that tracks every mutation. For a 1000-row table, that's 1000 proxies (and however many nested proxies per row). Most of it is overhead. The component only ever reassigns ",[14,3425,3426],{},".value"," wholesale, never mutates individual rows in place. ",[14,3429,2759],{}," says \"track when the reference changes, don't recurse.\" Which is exactly the contract the component needs.",[10,3432,3433,3434,3437,3438,3440],{},"The cloning is the second half of that story. ",[14,3435,3436],{},"_.cloneDeep(globalProps.items)"," snapshots the parent's data at assignment time; the table operates on its copy. This matters for sort: when the user sorts, the table mutates its ",[14,3439,3303],{}," without affecting the parent's array. Skipping the clone would turn a visual sort into a mutation on the parent's state, which is where the worst class of \"the server-side export didn't match what was on screen\" bugs come from.",[10,3442,3443,3444,229],{},"Re-watching uses ",[14,3445,3297],{},[40,3447,3449],{"className":186,"code":3448,"language":188,"meta":45,"style":45},"debouncedWatch(\n  () => globalProps.items,\n  () => (localItems.value = _.cloneDeep(globalProps.items)),\n  { deep: true, debounce: 100 }\n);\n\ndebouncedWatch(\n  () => globalProps.selectedItems,\n  () => (localSelectedItems.value = _.cloneDeep(globalProps.selectedItems)),\n  { deep: true, immediate: true, debounce: 50 }\n);\n",[14,3450,3451,3457,3467,3485,3502,3506,3510,3516,3525,3543,3561],{"__ignoreMap":45},[49,3452,3453,3455],{"class":51,"line":52},[49,3454,3297],{"class":202},[49,3456,978],{"class":206},[49,3458,3459,3462,3464],{"class":51,"line":58},[49,3460,3461],{"class":206},"  () ",[49,3463,361],{"class":195},[49,3465,3466],{"class":206}," globalProps.items,\n",[49,3468,3469,3471,3473,3476,3478,3480,3482],{"class":51,"line":64},[49,3470,3461],{"class":206},[49,3472,361],{"class":195},[49,3474,3475],{"class":206}," (localItems.value ",[49,3477,638],{"class":195},[49,3479,3155],{"class":206},[49,3481,3158],{"class":202},[49,3483,3484],{"class":206},"(globalProps.items)),\n",[49,3486,3487,3490,3493,3496,3499],{"class":51,"line":70},[49,3488,3489],{"class":206},"  { deep: ",[49,3491,3492],{"class":216},"true",[49,3494,3495],{"class":206},", debounce: ",[49,3497,3498],{"class":216},"100",[49,3500,3501],{"class":206}," }\n",[49,3503,3504],{"class":51,"line":76},[49,3505,1970],{"class":206},[49,3507,3508],{"class":51,"line":82},[49,3509,116],{"emptyLinePlaceholder":115},[49,3511,3512,3514],{"class":51,"line":88},[49,3513,3297],{"class":202},[49,3515,978],{"class":206},[49,3517,3518,3520,3522],{"class":51,"line":94},[49,3519,3461],{"class":206},[49,3521,361],{"class":195},[49,3523,3524],{"class":206}," globalProps.selectedItems,\n",[49,3526,3527,3529,3531,3534,3536,3538,3540],{"class":51,"line":100},[49,3528,3461],{"class":206},[49,3530,361],{"class":195},[49,3532,3533],{"class":206}," (localSelectedItems.value ",[49,3535,638],{"class":195},[49,3537,3155],{"class":206},[49,3539,3158],{"class":202},[49,3541,3542],{"class":206},"(globalProps.selectedItems)),\n",[49,3544,3545,3547,3549,3552,3554,3556,3559],{"class":51,"line":106},[49,3546,3489],{"class":206},[49,3548,3492],{"class":216},[49,3550,3551],{"class":206},", immediate: ",[49,3553,3492],{"class":216},[49,3555,3495],{"class":206},[49,3557,3558],{"class":216},"50",[49,3560,3501],{"class":206},[49,3562,3563],{"class":51,"line":112},[49,3564,1970],{"class":206},[10,3566,3567],{},"Why debounce? Because parent state often thrashes. A filter change at the parent level can trigger three re-renders in quick succession: form update, query string update, fetched response replacing the placeholder. Without a debounce, the table would clone its items three times in a single frame. 100ms for items and 50ms for selection are numbers I landed on by eyeballing the production behavior; they're not hallowed. If I had the appetite I'd make them props.",[10,3569,3570,3571,3574],{},"The columns\u002Factions watcher is ",[503,3572,3573],{},"not"," debounced:",[40,3576,3578],{"className":186,"code":3577,"language":188,"meta":45,"style":45},"localColumns.value = handleColumns();\nwatch(\n  () => [globalProps.columns, globalProps.actions],\n  () => {\n    localActions.value = _.cloneDeep(globalProps.actions);\n    localColumns.value = handleColumns();\n  },\n  { deep: true, immediate: true }\n);\n",[14,3579,3580,3592,3599,3608,3616,3630,3641,3646,3658],{"__ignoreMap":45},[49,3581,3582,3585,3587,3590],{"class":51,"line":52},[49,3583,3584],{"class":206},"localColumns.value ",[49,3586,638],{"class":195},[49,3588,3589],{"class":202}," handleColumns",[49,3591,1998],{"class":206},[49,3593,3594,3597],{"class":51,"line":58},[49,3595,3596],{"class":202},"watch",[49,3598,978],{"class":206},[49,3600,3601,3603,3605],{"class":51,"line":64},[49,3602,3461],{"class":206},[49,3604,361],{"class":195},[49,3606,3607],{"class":206}," [globalProps.columns, globalProps.actions],\n",[49,3609,3610,3612,3614],{"class":51,"line":70},[49,3611,3461],{"class":206},[49,3613,361],{"class":195},[49,3615,967],{"class":206},[49,3617,3618,3621,3623,3625,3627],{"class":51,"line":76},[49,3619,3620],{"class":206},"    localActions.value ",[49,3622,638],{"class":195},[49,3624,3155],{"class":206},[49,3626,3158],{"class":202},[49,3628,3629],{"class":206},"(globalProps.actions);\n",[49,3631,3632,3635,3637,3639],{"class":51,"line":82},[49,3633,3634],{"class":206},"    localColumns.value ",[49,3636,638],{"class":195},[49,3638,3589],{"class":202},[49,3640,1998],{"class":206},[49,3642,3643],{"class":51,"line":88},[49,3644,3645],{"class":206},"  },\n",[49,3647,3648,3650,3652,3654,3656],{"class":51,"line":94},[49,3649,3489],{"class":206},[49,3651,3492],{"class":216},[49,3653,3551],{"class":206},[49,3655,3492],{"class":216},[49,3657,3501],{"class":206},[49,3659,3660],{"class":51,"line":100},[49,3661,1970],{"class":206},[10,3663,3664,3665,3668],{},"Column and action changes are rare. Usually they're declared once at the top of a ",[14,3666,3667],{},"\u003Cscript setup>"," and never touched again. A debounce would introduce perceptible lag for the uncommon case where they do change (e.g. an admin toggling a column visibility). The asymmetry (debounce items, don't debounce columns) maps to how consumers actually use the component.",[29,3670,3672],{"id":3671},"loading-empty-paginated-the-surrounding-chrome","Loading, empty, paginated: the surrounding chrome",[10,3674,3675],{},"The surrounding chrome is a small story that nobody writes down.",[10,3677,3678,3679,229],{},"The loading state is an absolutely-positioned overlay with ",[14,3680,3681],{},"pointer-events-none",[40,3683,3685],{"className":587,"code":3684,"language":589,"meta":45,"style":45},"\u003Cdiv\n  v-if=\"globalProps.loading\"\n  class=\"bg-white\u002F70 flex pointer-events-none items-center inset-0 justify-center absolute z-10 backdrop-blur-[1px]\"\n>\n  \u003CLoadingComp \u002F>\n\u003C\u002Fdiv>\n",[14,3686,3687,3694,3710,3720,3724,3729],{"__ignoreMap":45},[49,3688,3689,3691],{"class":51,"line":52},[49,3690,207],{"class":206},[49,3692,3693],{"class":598},"div\n",[49,3695,3696,3699,3701,3704,3707],{"class":51,"line":58},[49,3697,3698],{"class":195},"  v-if",[49,3700,638],{"class":206},[49,3702,3703],{"class":310},"\"",[49,3705,3706],{"class":206},"globalProps.loading",[49,3708,3709],{"class":310},"\"\n",[49,3711,3712,3715,3717],{"class":51,"line":64},[49,3713,3714],{"class":202},"  class",[49,3716,638],{"class":206},[49,3718,3719],{"class":310},"\"bg-white\u002F70 flex pointer-events-none items-center inset-0 justify-center absolute z-10 backdrop-blur-[1px]\"\n",[49,3721,3722],{"class":51,"line":70},[49,3723,602],{"class":206},[49,3725,3726],{"class":51,"line":76},[49,3727,3728],{"class":206},"  \u003CLoadingComp \u002F>\n",[49,3730,3731,3733,3736],{"class":51,"line":82},[49,3732,738],{"class":206},[49,3734,3735],{"class":598},"div",[49,3737,602],{"class":206},[10,3739,3740,3742],{},[14,3741,3681],{}," is the detail. Without it, the overlay intercepts clicks on the rows underneath. Even though it's visually translucent, the user can't interact with the blurred table. The intent is \"show that something's happening, don't block the user from selecting a row they can still see.\" The overlay is just a hint.",[10,3744,3745],{},"The empty state is a row inside the table, not a sibling:",[40,3747,3749],{"className":587,"code":3748,"language":589,"meta":45,"style":45},"\u003CATBody :columns=\"localColumns\" :items=\"localItems\">\n  \u003Ctr v-if=\"localItems.length === 0 && !globalProps.loading\">\n    \u003Ctd :colspan=\"localColumns.length\" class=\"p-4\">\n      \u003CNoData :title=\"globalProps.noDataTitle\" \u002F>\n    \u003C\u002Ftd>\n  \u003C\u002Ftr>\n\u003C\u002FATBody>\n",[14,3750,3751,3785,3790,3795,3800,3805,3809],{"__ignoreMap":45},[49,3752,3753,3755,3757,3759,3762,3764,3766,3768,3770,3772,3775,3777,3779,3781,3783],{"class":51,"line":52},[49,3754,207],{"class":206},[49,3756,556],{"class":598},[49,3758,992],{"class":206},[49,3760,3761],{"class":202},"columns",[49,3763,638],{"class":206},[49,3765,3703],{"class":310},[49,3767,3306],{"class":206},[49,3769,3703],{"class":310},[49,3771,992],{"class":206},[49,3773,3774],{"class":202},"items",[49,3776,638],{"class":206},[49,3778,3703],{"class":310},[49,3780,3303],{"class":206},[49,3782,3703],{"class":310},[49,3784,602],{"class":206},[49,3786,3787],{"class":51,"line":58},[49,3788,3789],{"class":206},"  \u003Ctr v-if=\"localItems.length === 0 && !globalProps.loading\">\n",[49,3791,3792],{"class":51,"line":64},[49,3793,3794],{"class":206},"    \u003Ctd :colspan=\"localColumns.length\" class=\"p-4\">\n",[49,3796,3797],{"class":51,"line":70},[49,3798,3799],{"class":206},"      \u003CNoData :title=\"globalProps.noDataTitle\" \u002F>\n",[49,3801,3802],{"class":51,"line":76},[49,3803,3804],{"class":206},"    \u003C\u002Ftd>\n",[49,3806,3807],{"class":51,"line":82},[49,3808,849],{"class":206},[49,3810,3811,3813,3815],{"class":51,"line":88},[49,3812,738],{"class":206},[49,3814,556],{"class":598},[49,3816,602],{"class":206},[10,3818,3819,3820,3822,3823,3826,3827,3830],{},"Piping it through the ",[14,3821,569],{},"'s slot is how it inherits the table's border and spacing. A sibling ",[14,3824,3825],{},"\u003Cdiv>"," would need separate styles to line up; a ",[14,3828,3829],{},"\u003Ctr colspan>"," gets layout for free.",[10,3832,3833,3834,3837],{},"Pagination is optional and driven by the ",[14,3835,3836],{},"pagination"," prop:",[40,3839,3841],{"className":587,"code":3840,"language":589,"meta":45,"style":45},"\u003Cdiv v-if=\"globalProps.pagination && !globalProps.loading\">\n  \u003CAPagination\n    :model-value=\"globalProps.pagination.modelValue\"\n    :page-size=\"globalProps.pagination.pageSize\"\n    :total-items=\"globalProps.pagination.totalItems\"\n    @update:page-size=\"globalProps.pagination.onPageSizeChange\"\n    @update:model-value=\"globalProps.pagination.onUpdateCurrentPage\"\n  \u002F>\n\u003C\u002Fdiv>\n",[14,3842,3843,3869,3874,3879,3884,3889,3894,3899,3904],{"__ignoreMap":45},[49,3844,3845,3847,3849,3852,3854,3856,3859,3861,3863,3865,3867],{"class":51,"line":52},[49,3846,207],{"class":206},[49,3848,3735],{"class":598},[49,3850,3851],{"class":195}," v-if",[49,3853,638],{"class":206},[49,3855,3703],{"class":310},[49,3857,3858],{"class":206},"globalProps.pagination ",[49,3860,1704],{"class":195},[49,3862,3143],{"class":195},[49,3864,3706],{"class":206},[49,3866,3703],{"class":310},[49,3868,602],{"class":206},[49,3870,3871],{"class":51,"line":58},[49,3872,3873],{"class":206},"  \u003CAPagination\n",[49,3875,3876],{"class":51,"line":64},[49,3877,3878],{"class":206},"    :model-value=\"globalProps.pagination.modelValue\"\n",[49,3880,3881],{"class":51,"line":70},[49,3882,3883],{"class":206},"    :page-size=\"globalProps.pagination.pageSize\"\n",[49,3885,3886],{"class":51,"line":76},[49,3887,3888],{"class":206},"    :total-items=\"globalProps.pagination.totalItems\"\n",[49,3890,3891],{"class":51,"line":82},[49,3892,3893],{"class":206},"    @update:page-size=\"globalProps.pagination.onPageSizeChange\"\n",[49,3895,3896],{"class":51,"line":88},[49,3897,3898],{"class":206},"    @update:model-value=\"globalProps.pagination.onUpdateCurrentPage\"\n",[49,3900,3901],{"class":51,"line":94},[49,3902,3903],{"class":206},"  \u002F>\n",[49,3905,3906,3908,3910],{"class":51,"line":100},[49,3907,738],{"class":206},[49,3909,3735],{"class":598},[49,3911,602],{"class":206},[10,3913,3914,3915,3917,3918,283,3921,3924],{},"Two things worth flagging. The pagination component is hidden during loading. Without this, a page-size dropdown would be active during fetch, and a second rapid click could fire a second page request. And the callbacks are passed inside the ",[14,3916,3836],{}," object as fields (",[14,3919,3920],{},"onPageSizeChange",[14,3922,3923],{},"onUpdateCurrentPage",") rather than as separate props. I'm not sure that was the right call; emitting would have been more idiomatic Vue. But it keeps the \"pagination is a single config blob\" story clean.",[29,3926,3928],{"id":3927},"what-id-change-on-a-fourth-rewrite","What I'd change on a fourth rewrite",[10,3930,3931],{},"A few places where today's code isn't the version I'd write fresh:",[2162,3933,3934,3947,3963,3978,3984],{},[2165,3935,3936,3946],{},[2168,3937,891,3938,3940,3941,3943,3944,180],{},[14,3939,1338],{}," vs ",[14,3942,1342],{}," duplication in ",[14,3945,559],{}," Two separate props pointing at the same data. Collapsing to one prop would remove one spread and one branch. I left it because old call-sites depend on the name.",[2165,3948,3949,3954,3955,3958,3959,3962],{},[2168,3950,3951,3953],{},[14,3952,1782],{}," for default cells."," The footgun is real; the ergonomics are worth it. If I did it over I'd either go ",[14,3956,3957],{},"textContent"," by default and make HTML opt-in via a column field (",[14,3960,3961],{},"unsafe: true","), or commit fully to a slot-based approach. Both are refactors; neither is urgent.",[2165,3964,3965,3970,3971,3974,3975,3977],{},[2168,3966,891,3967,3969],{},[14,3968,769],{}," cast in the header."," Works, but an optional ",[14,3972,3973],{},"record?: S"," on ",[14,3976,559],{}," would be more honest. Headers don't have a record; say so in the type.",[2165,3979,3980,3983],{},[2168,3981,3982],{},"The hardcoded debounce values (50ms, 100ms)."," Should be props. Three minutes of work; I keep not doing it.",[2165,3985,3986,3996],{},[2168,3987,3988,3989,3992,3993,3995],{},"The computed ",[14,3990,3991],{},"Cell"," paired with ",[14,3994,1364],{}," keyed on sort state."," Still squinting at this one. Either the key is unnecessary (delete it and watch what happens) or it's hiding a timing bug in the patcher I should reproduce. I owe the code base that investigation.",[10,3998,3999,4000,4002,4003,4005,4006,4008],{},"None of those are the reason I'd recommend this pattern. The reason is smaller and duller: the column-as-data schema, the ",[14,4001,2759],{}," + clone + debounce rhythm, the Map-keyed selection, the ",[14,4004,2570],{}," on the per-row component. Each of them is the kind of thing you write the second time, after the first version has bitten you once. Which is the whole story of ",[14,4007,23],{},". The shape it landed in is the shape of a thing I stopped rewriting.",[4010,4011,4012],"style",{},"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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":45,"searchDepth":58,"depth":58,"links":4014},[4015,4017,4019,4024,4026,4027,4029,4031,4032,4034,4035],{"id":31,"depth":58,"text":4016},"From IColumn class to typed interface",{"id":549,"depth":58,"text":4018},"The render trio: ATHead, ATBody, ATCell",{"id":918,"depth":58,"text":4020,"children":4021},"ATCell: one component, two shapes, built via h()",[4022],{"id":1350,"depth":64,"text":4023},"The :key=\"sortValue?.applied\" that looks like a typo",{"id":1379,"depth":58,"text":4025},"handleValueBasedOnKey: dot-paths into rows",{"id":1796,"depth":58,"text":1797},{"id":2229,"depth":58,"text":4028},"Selection as a Map\u003Cstring, T>",{"id":2566,"depth":58,"text":4030},"Actions, and the markRaw that earned its keep",{"id":2832,"depth":58,"text":2833},{"id":3289,"depth":58,"text":4033},"Reactivity: shallowRef, cloneDeep, debouncedWatch",{"id":3671,"depth":58,"text":3672},{"id":3927,"depth":58,"text":3928},"2026-04-19","A typed generic Vue data table with schema-shaped columns, Map-keyed selection, markRaw per-row actions, and a three-click sort cycle. A full tour.","جولة كاملة في جدول بيانات Vue مُعمَّم بأنواع صارمة، بأعمدة مُعرَّفة كسكيمة، وتحديد صفوف قائم على Map، وأزرار إجراءات لكل صف عبر markRaw، ودورة فرز من ثلاث نقرات.",false,"md",null,[4043,4044,2570,2759,4045,4046,4047,3297,4048,4049],"Vue 3 data table","TypeScript generic component","row selection","column sorting","dot-path resolution","UI component","Nuxt","en",{},"\u002Fwrites\u002Fanatomy-of-adatatable",{"title":5,"description":4037},"writes\u002Fanatomy-of-adatatable","تشريح ADataTable، جدول بيانات مُعمَّم بأنواع في Vue","BpPooy2AKNC4CQ_dczfkSooGjmZ1gRZuFcU7i8jT2-I",[4058,5815],{"id":4059,"title":4060,"body":4061,"date":5797,"description":5798,"description_ar":5799,"draft":4039,"extension":4040,"image":4041,"keywords":5800,"lang":4050,"meta":5809,"navigation":115,"path":5810,"seo":5811,"stem":5812,"title_ar":5813,"updatedAt":4041,"__hash__":5814},"writes\u002Fwrites\u002Finside-a-skeleton-self-generating-skeleton-loaders.md","Inside @alikhalilll\u002Fa-skeleton, self-generating skeleton loaders",{"type":7,"value":4062,"toc":5786},[4063,4066,4072,4075,4079,4082,4087,4129,4134,4165,4193,4200,4214,4217,4221,4228,4249,4256,4267,4270,4310,4313,4399,4426,4432,4436,4443,4488,4513,4519,4522,4526,4536,4917,4954,4957,5026,5029,5144,5147,5170,5173,5177,5180,5186,5222,5241,5258,5268,5272,5320,5323,5330,5406,5419,5423,5466,5472,5483,5569,5576,5683,5705,5708,5712,5731,5749,5756,5760,5771,5777,5780,5783],[10,4064,4065],{},"Skeleton loaders are duplicate UI. You build a card, then you build a second card made of grey rectangles that approximates the first, then every time you refactor the real card you forget to refactor the placeholder. Three weeks later the heading is left-aligned in the real component and centred in the skeleton, and nobody notices until a designer scrolls past it on staging.",[10,4067,4068,4071],{},[14,4069,4070],{},"@alikhalilll\u002Fa-skeleton"," is built around a different premise: the skeleton derives itself from the real component. You wrap your card once, and the library figures out the placeholder by reading the card. No parallel template. No grey rectangles to keep in sync. The refactor you do on the card is the refactor the skeleton picks up on the next render.",[10,4073,4074],{},"This post is what's underneath that: the authoring contract that makes it work, the three rendering strategies that pay for different trade-offs, and the capture pipeline behind clone mode, which is the part I'd carry into any future \"I need a copy of how this looks right now\" problem.",[29,4076,4078],{"id":4077},"the-contract-keep-tags-unconditional-gate-content","The contract: keep tags unconditional, gate content",[10,4080,4081],{},"Before anything else, there's one rule. It's the load-bearing piece for every strategy below, so I'm putting it at the top.",[10,4083,4084],{},[2168,4085,4086],{},"Wrong:",[40,4088,4090],{"className":587,"code":4089,"language":589,"meta":45,"style":45},"\u003Cdiv v-if=\"user\">\n  \u003Ch3>{{ user.name }}\u003C\u002Fh3>\n  \u003Cp>{{ user.bio }}\u003C\u002Fp>\n\u003C\u002Fdiv>\n",[14,4091,4092,4111,4116,4121],{"__ignoreMap":45},[49,4093,4094,4096,4098,4100,4102,4104,4107,4109],{"class":51,"line":52},[49,4095,207],{"class":206},[49,4097,3735],{"class":598},[49,4099,3851],{"class":195},[49,4101,638],{"class":206},[49,4103,3703],{"class":310},[49,4105,4106],{"class":206},"user",[49,4108,3703],{"class":310},[49,4110,602],{"class":206},[49,4112,4113],{"class":51,"line":58},[49,4114,4115],{"class":206},"  \u003Ch3>{{ user.name }}\u003C\u002Fh3>\n",[49,4117,4118],{"class":51,"line":64},[49,4119,4120],{"class":206},"  \u003Cp>{{ user.bio }}\u003C\u002Fp>\n",[49,4122,4123,4125,4127],{"class":51,"line":70},[49,4124,738],{"class":206},[49,4126,3735],{"class":598},[49,4128,602],{"class":206},[10,4130,4131],{},[2168,4132,4133],{},"Right:",[40,4135,4137],{"className":587,"code":4136,"language":589,"meta":45,"style":45},"\u003Cdiv>\n  \u003Ch3>{{ user?.name }}\u003C\u002Fh3>\n  \u003Cp>{{ user?.bio }}\u003C\u002Fp>\n\u003C\u002Fdiv>\n",[14,4138,4139,4147,4152,4157],{"__ignoreMap":45},[49,4140,4141,4143,4145],{"class":51,"line":52},[49,4142,207],{"class":206},[49,4144,3735],{"class":598},[49,4146,602],{"class":206},[49,4148,4149],{"class":51,"line":58},[49,4150,4151],{"class":206},"  \u003Ch3>{{ user?.name }}\u003C\u002Fh3>\n",[49,4153,4154],{"class":51,"line":64},[49,4155,4156],{"class":206},"  \u003Cp>{{ user?.bio }}\u003C\u002Fp>\n",[49,4158,4159,4161,4163],{"class":51,"line":70},[49,4160,738],{"class":206},[49,4162,3735],{"class":598},[49,4164,602],{"class":206},[10,4166,4167,4168,4171,4172,4175,4176,4178,4179,4182,4183,283,4186,283,4189,4192],{},"The walker, whether it's reading the live DOM or the vnode tree, needs to see the same shape during loading as it'll see after data arrives. If the heading is wrapped in ",[14,4169,4170],{},"v-if=\"user\"",", the walker sees a comment node during loading and an ",[14,4173,4174],{},"\u003Ch3>"," after, and it has no way to predict the placeholder geometry. With the optional-chained interpolation, the ",[14,4177,4174],{}," is always there; its text is empty during loading, and the walker classifies empty text-owner tags (",[14,4180,4181],{},"\u003Ch1>","–",[14,4184,4185],{},"\u003Ch6>",[14,4187,4188],{},"\u003Cp>",[14,4190,4191],{},"\u003Cspan>",", etc.) as shimmer text bars at the heading's natural rendered height.",[10,4194,4195,4196,4199],{},"Tags are the schema. Content is the payload. You can swap the payload to ",[14,4197,4198],{},"null"," without the schema noticing. That single discipline is what unlocks the rest.",[10,4201,4202,4203,4206,4207,4209,4210,4213],{},"The other half of the contract is the ",[14,4204,4205],{},"data-skeleton-ignore"," escape hatch. If part of your component is decorative chrome (a gradient background, an always-present brand badge, an SVG flourish) you don't want it replaced by a grey block. Mark it ",[14,4208,4205],{}," and the walker treats it as invisible; the real element renders verbatim through both states. The corollary, ",[14,4211,4212],{},"data-skeleton-stop",", says \"don't recurse into this; render it as one block.\" Useful for atomic widgets where the inner structure is noise.",[10,4215,4216],{},"Two rules, two attributes. That's the whole authoring surface you ever have to learn.",[29,4218,4220],{"id":4219},"clone-mode-snapshot-the-final-paint-replay-the-pixels","Clone mode: snapshot the final paint, replay the pixels",[10,4222,4223,4224,4227],{},"Clone is the default, and it's the breakthrough. Most skeleton libraries get into trouble because they try to reason about the CSS cascade. They look at class names. They look at inline styles. They make assumptions about border radius from a ",[14,4225,4226],{},"rounded-*"," prefix. They guess.",[10,4229,4230,4231,4234,4235,4238,4239,4242,4243,4245,4246,4248],{},"Clone mode stops guessing. It mounts the slot off-screen in a ",[14,4232,4233],{},"visibility: hidden"," capture host, lets the browser do its full layout and style resolution, and then reads the ",[2168,4236,4237],{},"final computed result"," for every element via ",[14,4240,4241],{},"getComputedStyle()",". Whatever your styling system is (Tailwind utilities, scoped styles with hashed class names, CSS-in-JS at runtime, DaisyUI semantic tokens, inline ",[14,4244,4010],{}," attributes set by a parent), by the time ",[14,4247,4241],{}," runs, all of it has collapsed into one canonical answer per property. The engine snapshots that answer and replays it.",[10,4250,4251,4252,4255],{},"The captured surface is comprehensive. Per-edge borders (so a card with a different ",[14,4253,4254],{},"border-bottom"," reads correctly). Per-corner radii (so an asymmetric pill shape doesn't degrade to a generic rounded rect). Background colour and background image. Box-shadow. Opacity. Filter. Transform. Typography. Mix-blend-mode. Each captured node carries its own frozen inline-style payload; the render loop allocates nothing per node, it just reads what's there.",[10,4257,4258,4259,4262,4263,4266],{},"Text is the part that's easy to get wrong, and the part ",[14,4260,4261],{},"a-skeleton"," gets right. Instead of guessing wrap points (\"assume text wraps every N characters, assume the last line is 70% width\"), the engine uses ",[14,4264,4265],{},"Range.getClientRects()"," to capture the exact rendered rectangle of every text line: left, top, width, height. A three-line centred heading on desktop becomes a two-line centred heading on mobile, and the captured rects shrink to match. RTL last-line positions, justified text, manual line breaks: all of them replay 1:1 because they were measured, not heuristic'd.",[10,4268,4269],{},"The wrapper API for all of this is one prop:",[40,4271,4273],{"className":587,"code":4272,"language":589,"meta":45,"style":45},"\u003CASkeleton :loading=\"loading\">\n  \u003CSomeRichComponent \u002F>\n\u003C\u002FASkeleton>\n",[14,4274,4275,4297,4302],{"__ignoreMap":45},[49,4276,4277,4279,4282,4284,4287,4289,4291,4293,4295],{"class":51,"line":52},[49,4278,207],{"class":206},[49,4280,4281],{"class":598},"ASkeleton",[49,4283,992],{"class":206},[49,4285,4286],{"class":202},"loading",[49,4288,638],{"class":206},[49,4290,3703],{"class":310},[49,4292,4286],{"class":206},[49,4294,3703],{"class":310},[49,4296,602],{"class":206},[49,4298,4299],{"class":51,"line":58},[49,4300,4301],{"class":206},"  \u003CSomeRichComponent \u002F>\n",[49,4303,4304,4306,4308],{"class":51,"line":64},[49,4305,738],{"class":206},[49,4307,4281],{"class":598},[49,4309,602],{"class":206},[10,4311,4312],{},"If you want to roll your own capture \u002F replay flow, the underlying function is a public export:",[40,4314,4318],{"className":4315,"code":4316,"language":4317,"meta":45,"style":45},"language-ts shiki shiki-themes github-light github-dark","import { captureSnapshot, type CaptureSnapshot } from '@alikhalilll\u002Fa-skeleton';\n\nconst snap: CaptureSnapshot = captureSnapshot(myRootEl, {\n  maxDepth: 12,\n  maxNodes: 800,\n  minSize: 4,\n});\n","ts",[14,4319,4320,4341,4345,4365,4375,4385,4395],{"__ignoreMap":45},[49,4321,4322,4325,4328,4330,4333,4336,4339],{"class":51,"line":52},[49,4323,4324],{"class":195},"import",[49,4326,4327],{"class":206}," { captureSnapshot, ",[49,4329,1822],{"class":195},[49,4331,4332],{"class":206}," CaptureSnapshot } ",[49,4334,4335],{"class":195},"from",[49,4337,4338],{"class":310}," '@alikhalilll\u002Fa-skeleton'",[49,4340,324],{"class":206},[49,4342,4343],{"class":51,"line":58},[49,4344,116],{"emptyLinePlaceholder":115},[49,4346,4347,4349,4352,4354,4357,4359,4362],{"class":51,"line":64},[49,4348,951],{"class":195},[49,4350,4351],{"class":216}," snap",[49,4353,229],{"class":195},[49,4355,4356],{"class":202}," CaptureSnapshot",[49,4358,213],{"class":195},[49,4360,4361],{"class":202}," captureSnapshot",[49,4363,4364],{"class":206},"(myRootEl, {\n",[49,4366,4367,4370,4373],{"class":51,"line":70},[49,4368,4369],{"class":206},"  maxDepth: ",[49,4371,4372],{"class":216},"12",[49,4374,998],{"class":206},[49,4376,4377,4380,4383],{"class":51,"line":76},[49,4378,4379],{"class":206},"  maxNodes: ",[49,4381,4382],{"class":216},"800",[49,4384,998],{"class":206},[49,4386,4387,4390,4393],{"class":51,"line":82},[49,4388,4389],{"class":206},"  minSize: ",[49,4391,4392],{"class":216},"4",[49,4394,998],{"class":206},[49,4396,4397],{"class":51,"line":88},[49,4398,1152],{"class":206},[10,4400,4401,283,4404,3310,4407,4410,4411,4414,4415,4418,4419,4422,4423,4425],{},[14,4402,4403],{},"maxDepth",[14,4405,4406],{},"maxNodes",[14,4408,4409],{},"minSize"," are the budgets. They're the reason a 5,000-row table doesn't lock up the main thread. The walk bails when it hits the cap, sets ",[14,4412,4413],{},"truncated: true",", and the wrapper logs a one-time ",[14,4416,4417],{},"console.warn"," per ",[14,4420,4421],{},"cacheKey"," so you notice during development instead of in production. ",[14,4424,4409],{}," drops anything smaller than four CSS pixels per axis, which kills off hairlines and spacer dots that would otherwise show up as confetti in the skeleton.",[10,4427,4428,4429,4431],{},"The trade-off clone mode pays is that it's client-side only. You need a real DOM, ",[14,4430,4241],{},", and a browser to read the final paint. On the server, the slot's real markup renders normally; the snapshot + replay only kick in on hydration. If you need a server-rendered placeholder before hydration finishes, that's what mirror mode is for.",[29,4433,4435],{"id":4434},"mirror-mode-walk-the-vnodes-ship-to-the-server","Mirror mode: walk the vnodes, ship to the server",[10,4437,4438,4439,4442],{},"Mirror swaps the engine. Instead of mounting the slot and reading the DOM, it walks the vnode tree at render time. No ",[14,4440,4441],{},"window"," access. Hydration-clean. Server-renderable.",[40,4444,4446],{"className":587,"code":4445,"language":589,"meta":45,"style":45},"\u003CASkeleton mode=\"mirror\" :loading=\"loading\">\n  \u003CSomeRichComponent \u002F>\n\u003C\u002FASkeleton>\n",[14,4447,4448,4476,4480],{"__ignoreMap":45},[49,4449,4450,4452,4454,4457,4459,4462,4464,4466,4468,4470,4472,4474],{"class":51,"line":52},[49,4451,207],{"class":206},[49,4453,4281],{"class":598},[49,4455,4456],{"class":202}," mode",[49,4458,638],{"class":206},[49,4460,4461],{"class":310},"\"mirror\"",[49,4463,992],{"class":206},[49,4465,4286],{"class":202},[49,4467,638],{"class":206},[49,4469,3703],{"class":310},[49,4471,4286],{"class":206},[49,4473,3703],{"class":310},[49,4475,602],{"class":206},[49,4477,4478],{"class":51,"line":58},[49,4479,4301],{"class":206},[49,4481,4482,4484,4486],{"class":51,"line":64},[49,4483,738],{"class":206},[49,4485,4281],{"class":598},[49,4487,602],{"class":206},[10,4489,4490,4491,4494,4495,4498,4499,283,4502,283,4505,4508,4509,4512],{},"The walker preserves every element's tag and class. Text-bearing leaves become ",[14,4492,4493],{},"\u003Cspan class=\"a-skel-text-content\">"," (transparent text on a skeleton background) with ",[14,4496,4497],{},"box-decoration-break: clone"," so the shimmer wraps the exact rendered text width per line. Atomic and interactive tags (",[14,4500,4501],{},"\u003Cimg>",[14,4503,4504],{},"\u003Cbutton>",[14,4506,4507],{},"\u003Csvg>",") become ",[14,4510,4511],{},"\u003Cdiv class=\"a-skel-block\">"," sized from their class-derived dimensions.",[10,4514,4515,4516,4518],{},"Mirror's win is SSR. Its trade-off is fidelity. Without ",[14,4517,4241],{},", the walker can only see what's authored: the static class names and inline styles. If your component's identity comes from a hashed CSS-in-JS rule or a scoped style block, the mirror won't pick it up the way clone does. For most layouts, the static class is enough, and the SSR-safety is worth more than the last 5% of pixel match. For visually intricate components, clone is the right call.",[10,4520,4521],{},"Both modes share the same animation pipeline, the same cache, the same a11y baseline. Switching between them is a single prop.",[29,4523,4525],{"id":4524},"structural-mode-trees-in-normal-flow-persisted-between-sessions","Structural mode: trees in normal flow, persisted between sessions",[10,4527,4528,4529,4532,4533,180],{},"There's a third strategy that I reach for when the wrapper isn't my unit of orchestration: when I want a captured shape to survive across instances, persist between sessions, or reflow with its parent on viewport resize instead of being pinned to absolute coordinates. That's ",[14,4530,4531],{},"useSkeleton()"," + ",[14,4534,4535],{},"\u003CASkeletonLayer>",[40,4537,4539],{"className":587,"code":4538,"language":589,"meta":45,"style":45},"\u003Cscript setup lang=\"ts\">\nimport { computed, ref } from 'vue';\nimport { useSkeleton } from '@alikhalilll\u002Fa-skeleton';\n\nconst props = defineProps\u003C{ userId: string }>();\nconst user = ref(null);\nconst loading = computed(() => user.value === null);\nconst containerRef = ref\u003CHTMLElement | null>(null);\n\nconst { shape, clear } = useSkeleton({\n  cacheKey: `user-card:${props.userId}`,\n  target: () => (loading.value ? null : containerRef.value),\n  persist: true,\n});\n\nfetchUser(props.userId).then((u) => (user.value = u));\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv ref=\"containerRef\">\n    \u003CASkeletonLayer v-if=\"loading && shape\" :shape=\"shape\" \u002F>\n    \u003CColdStartFallback v-else-if=\"loading\" \u002F>\n    \u003CUserCard v-else :data=\"user\" \u002F>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[14,4540,4541,4561,4575,4588,4592,4617,4635,4660,4687,4691,4716,4735,4757,4766,4770,4774,4802,4810,4814,4822,4837,4861,4878,4899,4908],{"__ignoreMap":45},[49,4542,4543,4545,4548,4551,4554,4556,4559],{"class":51,"line":52},[49,4544,207],{"class":206},[49,4546,4547],{"class":598},"script",[49,4549,4550],{"class":202}," setup",[49,4552,4553],{"class":202}," lang",[49,4555,638],{"class":206},[49,4557,4558],{"class":310},"\"ts\"",[49,4560,602],{"class":206},[49,4562,4563,4565,4568,4570,4573],{"class":51,"line":58},[49,4564,4324],{"class":195},[49,4566,4567],{"class":206}," { computed, ref } ",[49,4569,4335],{"class":195},[49,4571,4572],{"class":310}," 'vue'",[49,4574,324],{"class":206},[49,4576,4577,4579,4582,4584,4586],{"class":51,"line":64},[49,4578,4324],{"class":195},[49,4580,4581],{"class":206}," { useSkeleton } ",[49,4583,4335],{"class":195},[49,4585,4338],{"class":310},[49,4587,324],{"class":206},[49,4589,4590],{"class":51,"line":70},[49,4591,116],{"emptyLinePlaceholder":115},[49,4593,4594,4596,4599,4601,4604,4607,4610,4612,4614],{"class":51,"line":76},[49,4595,951],{"class":195},[49,4597,4598],{"class":216}," props",[49,4600,213],{"class":195},[49,4602,4603],{"class":202}," defineProps",[49,4605,4606],{"class":206},"\u003C{ ",[49,4608,4609],{"class":225},"userId",[49,4611,229],{"class":195},[49,4613,232],{"class":216},[49,4615,4616],{"class":206}," }>();\n",[49,4618,4619,4621,4624,4626,4629,4631,4633],{"class":51,"line":82},[49,4620,951],{"class":195},[49,4622,4623],{"class":216}," user",[49,4625,213],{"class":195},[49,4627,4628],{"class":202}," ref",[49,4630,1072],{"class":206},[49,4632,4198],{"class":216},[49,4634,1970],{"class":206},[49,4636,4637,4639,4642,4644,4646,4648,4650,4653,4655,4658],{"class":51,"line":88},[49,4638,951],{"class":195},[49,4640,4641],{"class":216}," loading",[49,4643,213],{"class":195},[49,4645,959],{"class":202},[49,4647,962],{"class":206},[49,4649,361],{"class":195},[49,4651,4652],{"class":206}," user.value ",[49,4654,1296],{"class":195},[49,4656,4657],{"class":216}," null",[49,4659,1970],{"class":206},[49,4661,4662,4664,4667,4669,4671,4673,4676,4678,4680,4683,4685],{"class":51,"line":94},[49,4663,951],{"class":195},[49,4665,4666],{"class":216}," containerRef",[49,4668,213],{"class":195},[49,4670,4628],{"class":202},[49,4672,207],{"class":206},[49,4674,4675],{"class":202},"HTMLElement",[49,4677,235],{"class":195},[49,4679,4657],{"class":216},[49,4681,4682],{"class":206},">(",[49,4684,4198],{"class":216},[49,4686,1970],{"class":206},[49,4688,4689],{"class":51,"line":100},[49,4690,116],{"emptyLinePlaceholder":115},[49,4692,4693,4695,4697,4700,4702,4705,4708,4710,4713],{"class":51,"line":106},[49,4694,951],{"class":195},[49,4696,2855],{"class":206},[49,4698,4699],{"class":216},"shape",[49,4701,283],{"class":206},[49,4703,4704],{"class":216},"clear",[49,4706,4707],{"class":206}," } ",[49,4709,638],{"class":195},[49,4711,4712],{"class":202}," useSkeleton",[49,4714,4715],{"class":206},"({\n",[49,4717,4718,4721,4724,4727,4729,4731,4733],{"class":51,"line":112},[49,4719,4720],{"class":206},"  cacheKey: ",[49,4722,4723],{"class":310},"`user-card:${",[49,4725,4726],{"class":206},"props",[49,4728,180],{"class":310},[49,4730,4609],{"class":206},[49,4732,2048],{"class":310},[49,4734,998],{"class":206},[49,4736,4737,4740,4743,4745,4748,4750,4752,4754],{"class":51,"line":119},[49,4738,4739],{"class":202},"  target",[49,4741,4742],{"class":206},": () ",[49,4744,361],{"class":195},[49,4746,4747],{"class":206}," (loading.value ",[49,4749,986],{"class":195},[49,4751,4657],{"class":216},[49,4753,992],{"class":195},[49,4755,4756],{"class":206}," containerRef.value),\n",[49,4758,4759,4762,4764],{"class":51,"line":125},[49,4760,4761],{"class":206},"  persist: ",[49,4763,3492],{"class":216},[49,4765,998],{"class":206},[49,4767,4768],{"class":51,"line":131},[49,4769,1152],{"class":206},[49,4771,4772],{"class":51,"line":137},[49,4773,116],{"emptyLinePlaceholder":115},[49,4775,4776,4779,4782,4785,4787,4790,4792,4794,4797,4799],{"class":51,"line":143},[49,4777,4778],{"class":202},"fetchUser",[49,4780,4781],{"class":206},"(props.userId).",[49,4783,4784],{"class":202},"then",[49,4786,1675],{"class":206},[49,4788,4789],{"class":225},"u",[49,4791,439],{"class":206},[49,4793,361],{"class":195},[49,4795,4796],{"class":206}," (user.value ",[49,4798,638],{"class":195},[49,4800,4801],{"class":206}," u));\n",[49,4803,4804,4806,4808],{"class":51,"line":149},[49,4805,738],{"class":206},[49,4807,4547],{"class":598},[49,4809,602],{"class":206},[49,4811,4812],{"class":51,"line":155},[49,4813,116],{"emptyLinePlaceholder":115},[49,4815,4816,4818,4820],{"class":51,"line":161},[49,4817,207],{"class":206},[49,4819,599],{"class":598},[49,4821,602],{"class":206},[49,4823,4824,4826,4828,4830,4832,4835],{"class":51,"line":166},[49,4825,607],{"class":206},[49,4827,3735],{"class":598},[49,4829,4628],{"class":202},[49,4831,638],{"class":206},[49,4833,4834],{"class":310},"\"containerRef\"",[49,4836,602],{"class":206},[49,4838,4839,4841,4844,4846,4848,4851,4854,4856,4859],{"class":51,"line":3262},[49,4840,617],{"class":206},[49,4842,4843],{"class":598},"ASkeletonLayer",[49,4845,3851],{"class":202},[49,4847,638],{"class":206},[49,4849,4850],{"class":310},"\"loading && shape\"",[49,4852,4853],{"class":202}," :shape",[49,4855,638],{"class":206},[49,4857,4858],{"class":310},"\"shape\"",[49,4860,1194],{"class":206},[49,4862,4863,4865,4868,4871,4873,4876],{"class":51,"line":3274},[49,4864,617],{"class":206},[49,4866,4867],{"class":598},"ColdStartFallback",[49,4869,4870],{"class":202}," v-else-if",[49,4872,638],{"class":206},[49,4874,4875],{"class":310},"\"loading\"",[49,4877,1194],{"class":206},[49,4879,4881,4883,4886,4889,4892,4894,4897],{"class":51,"line":4880},23,[49,4882,617],{"class":206},[49,4884,4885],{"class":598},"UserCard",[49,4887,4888],{"class":202}," v-else",[49,4890,4891],{"class":202}," :data",[49,4893,638],{"class":206},[49,4895,4896],{"class":310},"\"user\"",[49,4898,1194],{"class":206},[49,4900,4902,4904,4906],{"class":51,"line":4901},24,[49,4903,729],{"class":206},[49,4905,3735],{"class":598},[49,4907,602],{"class":206},[49,4909,4911,4913,4915],{"class":51,"line":4910},25,[49,4912,738],{"class":206},[49,4914,599],{"class":598},[49,4916,602],{"class":206},[10,4918,4919,4920,4923,4924,283,4927,283,4930,283,4933,283,4936,283,4939,4942,4943,4945,4946,4949,4950,4953],{},"The shape is captured by ",[14,4921,4922],{},"walkStructural()",", a tree walker that preserves container tags, their original classes, and the resolved layout CSS (",[14,4925,4926],{},"display",[14,4928,4929],{},"flex-*",[14,4931,4932],{},"gap",[14,4934,4935],{},"padding",[14,4937,4938],{},"grid-*",[14,4940,4941],{},"box-sizing","). Leaves carry inline width\u002Fheight plus the visual signals (background, border, radius). When ",[14,4944,4535],{}," replays the tree, it does so in ",[2168,4947,4948],{},"normal flow",": the captured ",[14,4951,4952],{},"\u003Cdiv class=\"flex flex-col gap-4 p-4\">"," is rendered as a real flex container, and the shimmer blocks sit inside it as flex children. Resize the viewport and the layout reflows; the skeleton doesn't lag behind the way an absolute-positioned clone replay would.",[10,4955,4956],{},"Given this real markup:",[40,4958,4962],{"className":4959,"code":4960,"language":4961,"meta":45,"style":45},"language-html shiki shiki-themes github-light github-dark","\u003Cdiv class=\"flex flex-col gap-4 p-4\">\n  \u003Ch3>...\u003C\u002Fh3>\n  \u003Cp>...\u003C\u002Fp>\n  \u003Cbutton>...\u003C\u002Fbutton>\n\u003C\u002Fdiv>\n","html",[14,4963,4964,4980,4993,5005,5018],{"__ignoreMap":45},[49,4965,4966,4968,4970,4973,4975,4978],{"class":51,"line":52},[49,4967,207],{"class":206},[49,4969,3735],{"class":598},[49,4971,4972],{"class":202}," class",[49,4974,638],{"class":206},[49,4976,4977],{"class":310},"\"flex flex-col gap-4 p-4\"",[49,4979,602],{"class":206},[49,4981,4982,4984,4986,4989,4991],{"class":51,"line":58},[49,4983,607],{"class":206},[49,4985,1348],{"class":598},[49,4987,4988],{"class":206},">...\u003C\u002F",[49,4990,1348],{"class":598},[49,4992,602],{"class":206},[49,4994,4995,4997,4999,5001,5003],{"class":51,"line":64},[49,4996,607],{"class":206},[49,4998,10],{"class":598},[49,5000,4988],{"class":206},[49,5002,10],{"class":598},[49,5004,602],{"class":206},[49,5006,5007,5009,5012,5014,5016],{"class":51,"line":70},[49,5008,607],{"class":206},[49,5010,5011],{"class":598},"button",[49,5013,4988],{"class":206},[49,5015,5011],{"class":598},[49,5017,602],{"class":206},[49,5019,5020,5022,5024],{"class":51,"line":76},[49,5021,738],{"class":206},[49,5023,3735],{"class":598},[49,5025,602],{"class":206},[10,5027,5028],{},"The layer replays as:",[40,5030,5032],{"className":4959,"code":5031,"language":4961,"meta":45,"style":45},"\u003Cdiv\n  class=\"flex flex-col gap-4 p-4\"\n  style=\"display: flex; flex-direction: column; gap: 16px; padding: 16px; box-sizing: border-box\"\n>\n  \u003Cdiv class=\"a-skel\" style=\"width: 200px; height: 24px; ...\" \u002F>\n  \u003Cdiv class=\"a-skel\" style=\"width: 280px; height: 16px; ...\" \u002F>\n  \u003Cdiv class=\"a-skel\" style=\"width: 120px; height: 36px; ...\" \u002F>\n\u003C\u002Fdiv>\n",[14,5033,5034,5040,5049,5059,5063,5090,5113,5136],{"__ignoreMap":45},[49,5035,5036,5038],{"class":51,"line":52},[49,5037,207],{"class":206},[49,5039,3693],{"class":598},[49,5041,5042,5044,5046],{"class":51,"line":58},[49,5043,3714],{"class":202},[49,5045,638],{"class":206},[49,5047,5048],{"class":310},"\"flex flex-col gap-4 p-4\"\n",[49,5050,5051,5054,5056],{"class":51,"line":64},[49,5052,5053],{"class":202},"  style",[49,5055,638],{"class":206},[49,5057,5058],{"class":310},"\"display: flex; flex-direction: column; gap: 16px; padding: 16px; box-sizing: border-box\"\n",[49,5060,5061],{"class":51,"line":70},[49,5062,602],{"class":206},[49,5064,5065,5067,5069,5071,5073,5076,5079,5081,5084,5088],{"class":51,"line":76},[49,5066,607],{"class":206},[49,5068,3735],{"class":598},[49,5070,4972],{"class":202},[49,5072,638],{"class":206},[49,5074,5075],{"class":310},"\"a-skel\"",[49,5077,5078],{"class":202}," style",[49,5080,638],{"class":206},[49,5082,5083],{"class":310},"\"width: 200px; height: 24px; ...\"",[49,5085,5087],{"class":5086},"s7hpK"," \u002F",[49,5089,602],{"class":206},[49,5091,5092,5094,5096,5098,5100,5102,5104,5106,5109,5111],{"class":51,"line":82},[49,5093,607],{"class":206},[49,5095,3735],{"class":598},[49,5097,4972],{"class":202},[49,5099,638],{"class":206},[49,5101,5075],{"class":310},[49,5103,5078],{"class":202},[49,5105,638],{"class":206},[49,5107,5108],{"class":310},"\"width: 280px; height: 16px; ...\"",[49,5110,5087],{"class":5086},[49,5112,602],{"class":206},[49,5114,5115,5117,5119,5121,5123,5125,5127,5129,5132,5134],{"class":51,"line":88},[49,5116,607],{"class":206},[49,5118,3735],{"class":598},[49,5120,4972],{"class":202},[49,5122,638],{"class":206},[49,5124,5075],{"class":310},[49,5126,5078],{"class":202},[49,5128,638],{"class":206},[49,5130,5131],{"class":310},"\"width: 120px; height: 36px; ...\"",[49,5133,5087],{"class":5086},[49,5135,602],{"class":206},[49,5137,5138,5140,5142],{"class":51,"line":94},[49,5139,738],{"class":206},[49,5141,3735],{"class":598},[49,5143,602],{"class":206},[10,5145,5146],{},"The original class is preserved so utility-first CSS still applies; the resolved layout CSS is inlined as a fallback so the skeleton looks correct even when the stylesheet isn't loaded at the mount point. This matters if you're rendering the same captured shape into a different CSS context, like an email-style preview window or a server-rendered shell that doesn't pull Tailwind.",[10,5148,5149,5150,5153,5154,5157,5158,5161,5162,5165,5166,5169],{},"The other thing structural mode gives you is persistence. ",[14,5151,5152],{},"persist: true"," mirrors the captured shape into ",[14,5155,5156],{},"localStorage"," under the ",[14,5159,5160],{},"a-skeleton:s:"," prefix. Cold start on a return visit reads the cached shape and replays the skeleton on first paint: no flash of \"generic shimmer\" while the real component mounts and measures itself. The cache is schema-versioned (",[14,5163,5164],{},"v: 3"," for structural, ",[14,5167,5168],{},"v: 2"," for the legacy flat shape), so mismatched versions auto-purge on read. You can't replay yesterday's geometry through today's renderer if the data shape doesn't match.",[10,5171,5172],{},"The three strategies pick different default behaviours from the same primitives. Clone replays absolutely-positioned pixels. Mirror replays vnodes in the original tree. Structural replays containers in normal flow. One cache module, one theming surface, one a11y baseline, three engines.",[29,5174,5176],{"id":5175},"the-capture-cache-and-replay-pipeline","The capture, cache, and replay pipeline",[10,5178,5179],{},"The architectural story is the same across all three modes, even though the implementations diverge. Here's how a single loading cycle plays out, end to end.",[10,5181,5182,5185],{},[2168,5183,5184],{},"First paint, cache miss."," The wrapper mounts. There's no cached shape yet. In clone and mirror mode, the engine walks the slot's structure (vnodes for mirror, the off-screen DOM mount for clone) and produces a placeholder that mirrors the slot's tag structure. Containers preserve their semantic tags and layout CSS. Atomic leaves and text-bearing elements become shimmer placeholders. The user sees a structural skeleton that matches the real component's outline instantly, before any data has resolved.",[10,5187,5188,5191,5192,5195,5196,5199,5200,5203,5204,5206,5207,5209,5210,5213,5214,5216,5217,5219,5220,1669],{},[2168,5189,5190],{},"Data arrives."," The slot's real content renders. A ",[14,5193,5194],{},"ResizeObserver"," plus a ",[14,5197,5198],{},"requestAnimationFrame"," schedules a measurement pass. In clone mode, ",[14,5201,5202],{},"captureSnapshot()"," reads ",[14,5205,4241],{}," on every element and per-line text rects via ",[14,5208,4265],{},", then freezes the result into a ",[14,5211,5212],{},"CaptureSnapshot"," tree. In structural mode, ",[14,5215,4922],{}," produces a frozen tree where containers carry their layout CSS and leaves carry their dimensions and visual signals. Either way, the captured snapshot is now in memory (and optionally in ",[14,5218,5156],{}," if ",[14,5221,5152],{},[10,5223,5224,5227,5228,5230,5231,5233,5234,5237,5238,5240],{},[2168,5225,5226],{},"Next loading flip."," The user navigates back to the page, or triggers a refetch, or revisits the route on a return session. ",[14,5229,4286],{}," flips to ",[14,5232,3492],{},". The cached shape replays instantly, with no intermediate \"guess the shape\" step. In clone mode, ",[14,5235,5236],{},"\u003CASkeletonClone>"," renders a tree of absolutely-positioned divs each carrying its pre-frozen inline style; the render loop is allocation-free because every style payload was computed at capture time. In structural mode, ",[14,5239,4535],{}," renders the tree in normal flow, with the captured layout CSS driving the flex\u002Fgrid container behaviour.",[10,5242,5243,5244,5246,5247,5249,5250,5252,5253,4418,5255,5257],{},"The budgets matter at every step. ",[14,5245,4406],{}," (default 500–800 depending on the walker) is the cap on tree size. ",[14,5248,4403],{}," (default 12–16) is the recursion cap. ",[14,5251,4409],{}," (default 4 CSS pixels) is the per-leaf filter. These aren't arbitrary: they're chosen so a busy dashboard with hundreds of leaf elements still walks in a single rAF and the captured tree fits in a localStorage entry without blowing past the 5 MB quota. When a walk truncates, the wrapper logs a one-time ",[14,5254,4417],{},[14,5256,4421],{}," so you find out in dev rather than in production.",[10,5259,5260,5261,5264,5265,5267],{},"One layout read up front, then cached values for the rest of the walk. The ",[14,5262,5263],{},"getBoundingClientRect()"," and ",[14,5266,4241],{}," calls happen in a single top-down pass with no intervening writes, no layout thrash: one forced reflow, then the captured snapshot is durable.",[29,5269,5271],{"id":5270},"the-15-named-primitives","The 15 named primitives",[10,5273,5274,5275,283,5278,283,5281,283,5284,283,5287,283,5290,283,5293,283,5296,283,5299,283,5302,283,5305,283,5308,283,5311,283,5314,283,5317,180],{},"Self-generating doesn't fit every case. If your \"loading state\" doesn't have a real component to wrap (you're showing the skeleton before any data fetch starts, or the placeholder genuinely has different markup from the loaded view), the auto-capture approach has nothing to read. For those cases the package ships 15 named variants: ",[14,5276,5277],{},"\u003CASkeletonCard>",[14,5279,5280],{},"\u003CASkeletonText>",[14,5282,5283],{},"\u003CASkeletonHeading>",[14,5285,5286],{},"\u003CASkeletonAvatar>",[14,5288,5289],{},"\u003CASkeletonImage>",[14,5291,5292],{},"\u003CASkeletonVideo>",[14,5294,5295],{},"\u003CASkeletonButton>",[14,5297,5298],{},"\u003CASkeletonInput>",[14,5300,5301],{},"\u003CASkeletonChip>",[14,5303,5304],{},"\u003CASkeletonListItem>",[14,5306,5307],{},"\u003CASkeletonTable>",[14,5309,5310],{},"\u003CASkeletonChart>",[14,5312,5313],{},"\u003CASkeletonForm>",[14,5315,5316],{},"\u003CASkeletonArticle>",[14,5318,5319],{},"\u003CASkeletonDivider>",[10,5321,5322],{},"They're not a replacement for the auto-capture path; they're an exit ramp. Same animation pipeline, same theming tokens, same a11y baseline. Hand-authored when you need them, otherwise let the wrapper do the work.",[10,5324,5325,5326,5329],{},"There's also ",[14,5327,5328],{},"\u003CASkeletonBlock>"," for the case where none of the variants fit and you want to compose your own:",[40,5331,5333],{"className":587,"code":5332,"language":589,"meta":45,"style":45},"\u003Cdiv v-if=\"loading\" class=\"flex items-start gap-4 p-4\">\n  \u003CASkeletonBlock type=\"circle\" :w=\"64\" :h=\"64\" \u002F>\n  \u003Cdiv class=\"flex-1 space-y-2\">\n    \u003CASkeletonBlock type=\"text\" :w=\"160\" :h=\"18\" \u002F>\n    \u003CASkeletonBlock type=\"text\" :w=\"100\" :h=\"12\" \u002F>\n    \u003CASkeletonBlock type=\"text\" :lines=\"3\" :h=\"14\" class=\"!mt-3\" \u002F>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\n\u003CUserCard v-else :data=\"user\" \u002F>\n",[14,5334,5335,5360,5365,5370,5375,5380,5385,5393,5401],{"__ignoreMap":45},[49,5336,5337,5339,5341,5343,5345,5347,5349,5351,5353,5355,5358],{"class":51,"line":52},[49,5338,207],{"class":206},[49,5340,3735],{"class":598},[49,5342,3851],{"class":195},[49,5344,638],{"class":206},[49,5346,3703],{"class":310},[49,5348,4286],{"class":206},[49,5350,3703],{"class":310},[49,5352,4972],{"class":202},[49,5354,638],{"class":206},[49,5356,5357],{"class":310},"\"flex items-start gap-4 p-4\"",[49,5359,602],{"class":206},[49,5361,5362],{"class":51,"line":58},[49,5363,5364],{"class":206},"  \u003CASkeletonBlock type=\"circle\" :w=\"64\" :h=\"64\" \u002F>\n",[49,5366,5367],{"class":51,"line":64},[49,5368,5369],{"class":206},"  \u003Cdiv class=\"flex-1 space-y-2\">\n",[49,5371,5372],{"class":51,"line":70},[49,5373,5374],{"class":206},"    \u003CASkeletonBlock type=\"text\" :w=\"160\" :h=\"18\" \u002F>\n",[49,5376,5377],{"class":51,"line":76},[49,5378,5379],{"class":206},"    \u003CASkeletonBlock type=\"text\" :w=\"100\" :h=\"12\" \u002F>\n",[49,5381,5382],{"class":51,"line":82},[49,5383,5384],{"class":206},"    \u003CASkeletonBlock type=\"text\" :lines=\"3\" :h=\"14\" class=\"!mt-3\" \u002F>\n",[49,5386,5387,5389,5391],{"class":51,"line":88},[49,5388,729],{"class":206},[49,5390,3735],{"class":598},[49,5392,602],{"class":206},[49,5394,5395,5397,5399],{"class":51,"line":94},[49,5396,738],{"class":206},[49,5398,3735],{"class":598},[49,5400,602],{"class":206},[49,5402,5403],{"class":51,"line":100},[49,5404,5405],{"class":206},"\u003CUserCard v-else :data=\"user\" \u002F>\n",[10,5407,5408,5410,5411,5414,5415,5418],{},[14,5409,5328],{}," is flow-friendly. It composes with flex, grid, stack layouts. The ",[14,5412,5413],{},"lines"," prop on ",[14,5416,5417],{},"type=\"text\""," stacks N bars with the last at 70% width, which is the heuristic that's correct enough most of the time when you don't have a real component to measure.",[29,5420,5422],{"id":5421},"animations-and-theming-as-strategies","Animations and theming, as strategies",[10,5424,5425,5426,5429,5430,5433,5434,5437,5438,5441,5442,5445,5446,5449,5450,5453,5454,5457,5458,5461,5462,5465],{},"Four animation modes ship: ",[14,5427,5428],{},"shimmer"," (a gradient sweep on a ",[14,5431,5432],{},"::after"," pseudo-element, contained per block with ",[14,5435,5436],{},"overflow: hidden","), ",[14,5439,5440],{},"pulse"," (opacity cycling from 1 to ",[14,5443,5444],{},"--ak-skel-pulse-min"," and back), ",[14,5447,5448],{},"wave"," (a sliding gradient via ",[14,5451,5452],{},"background-position","), and ",[14,5455,5456],{},"none"," (static blocks). Each is a strategy, not a branch. You pass ",[14,5459,5460],{},"animation=\"pulse\""," and the right CSS class engages. No ",[14,5463,5464],{},"if (animation === 'pulse')"," chain in the component.",[10,5467,5468,5471],{},[14,5469,5470],{},"prefers-reduced-motion: reduce"," disables every animation automatically. The shimmer pseudo-element drops to a static low-opacity overlay; the pulse stops cycling. No component code involved; it's a media query in the stylesheet.",[10,5473,5474,5475,5478,5479,5482],{},"Theming is CSS variables. The tokens are scoped under ",[14,5476,5477],{},"--ak-skel-*"," (with backward-compat aliases under ",[14,5480,5481],{},"--ak-skeleton-*"," for v1 consumers):",[5484,5485,5486,5497],"table",{},[610,5487,5488],{},[620,5489,5490,5494],{},[5491,5492,5493],"th",{},"Token",[5491,5495,5496],{},"Used for",[792,5498,5499,5510,5520,5530,5540,5549,5559],{},[620,5500,5501,5507],{},[5502,5503,5504],"td",{},[14,5505,5506],{},"--ak-skel-base",[5502,5508,5509],{},"Block fill",[620,5511,5512,5517],{},[5502,5513,5514],{},[14,5515,5516],{},"--ak-skel-highlight",[5502,5518,5519],{},"Shimmer \u002F wave sweep colour",[620,5521,5522,5527],{},[5502,5523,5524],{},[14,5525,5526],{},"--ak-skel-radius",[5502,5528,5529],{},"Default block border radius",[620,5531,5532,5537],{},[5502,5533,5534],{},[14,5535,5536],{},"--ak-skel-duration",[5502,5538,5539],{},"Animation cycle length",[620,5541,5542,5546],{},[5502,5543,5544],{},[14,5545,5444],{},[5502,5547,5548],{},"Opacity at the trough of the pulse cycle",[620,5550,5551,5556],{},[5502,5552,5553],{},[14,5554,5555],{},"--ak-skel-ring",[5502,5557,5558],{},"Subtle 1-px inset ring colour",[620,5560,5561,5566],{},[5502,5562,5563],{},[14,5564,5565],{},"--ak-skel-icon",[5502,5567,5568],{},"Placeholder icon colour (image \u002F video variants)",[10,5570,5571,5572,5575],{},"Override them on ",[14,5573,5574],{},":root",", on a wrapper class for multi-tenant themes, or inline on a single instance:",[40,5577,5581],{"className":5578,"code":5579,"language":5580,"meta":45,"style":45},"language-css shiki shiki-themes github-light github-dark",".tenant-acme {\n  --ak-skel-base: hsl(220 30% 18%);\n  --ak-skel-highlight: hsl(220 60% 60% \u002F 0.35);\n  --ak-skel-radius: 0.5rem;\n  --ak-skel-duration: 2s;\n}\n","css",[14,5582,5583,5590,5619,5649,5664,5679],{"__ignoreMap":45},[49,5584,5585,5588],{"class":51,"line":52},[49,5586,5587],{"class":202},".tenant-acme",[49,5589,967],{"class":206},[49,5591,5592,5595,5598,5601,5603,5606,5609,5612,5615,5617],{"class":51,"line":58},[49,5593,5594],{"class":225},"  --ak-skel-base",[49,5596,5597],{"class":206},": ",[49,5599,5600],{"class":216},"hsl",[49,5602,1072],{"class":206},[49,5604,5605],{"class":216},"220",[49,5607,5608],{"class":216}," 30",[49,5610,5611],{"class":195},"%",[49,5613,5614],{"class":216}," 18",[49,5616,5611],{"class":195},[49,5618,1970],{"class":206},[49,5620,5621,5624,5626,5628,5630,5632,5635,5637,5639,5641,5644,5647],{"class":51,"line":64},[49,5622,5623],{"class":225},"  --ak-skel-highlight",[49,5625,5597],{"class":206},[49,5627,5600],{"class":216},[49,5629,1072],{"class":206},[49,5631,5605],{"class":216},[49,5633,5634],{"class":216}," 60",[49,5636,5611],{"class":195},[49,5638,5634],{"class":216},[49,5640,5611],{"class":195},[49,5642,5643],{"class":206}," \u002F ",[49,5645,5646],{"class":216},"0.35",[49,5648,1970],{"class":206},[49,5650,5651,5654,5656,5659,5662],{"class":51,"line":70},[49,5652,5653],{"class":225},"  --ak-skel-radius",[49,5655,5597],{"class":206},[49,5657,5658],{"class":216},"0.5",[49,5660,5661],{"class":195},"rem",[49,5663,324],{"class":206},[49,5665,5666,5669,5671,5674,5677],{"class":51,"line":76},[49,5667,5668],{"class":225},"  --ak-skel-duration",[49,5670,5597],{"class":206},[49,5672,5673],{"class":216},"2",[49,5675,5676],{"class":195},"s",[49,5678,324],{"class":206},[49,5680,5681],{"class":51,"line":82},[49,5682,169],{"class":206},[10,5684,5685,5686,5689,5690,5693,5694,5697,5698,5700,5701,5704],{},"Dark mode is automatic via ",[14,5687,5688],{},":where(.dark)"," selectors that retint the tokens (Tailwind's ",[14,5691,5692],{},".dark"," class, shadcn's, ",[14,5695,5696],{},"nuxt-color-mode","'s, anything that lands a ",[14,5699,5692],{}," ancestor), plus a ",[14,5702,5703],{},"@media (prefers-color-scheme: dark)"," fallback for the case where no explicit class is on the tree.",[10,5706,5707],{},"The point of doing it this way is that no consumer should ever have to override an internal class. Every visual decision is a token; tokens are inheritable through the cascade; per-instance overrides are inline styles. The component owns the structure and the animation; you own the look.",[29,5709,5711],{"id":5710},"a11y-you-dont-have-to-configure","A11y you don't have to configure",[10,5713,5714,5715,5718,5719,5722,5723,5726,5727,5730],{},"The defaults are the ones you'd want anyway. Every wrapper and variant root carries ",[14,5716,5717],{},"role=\"status\""," while loading, ",[14,5720,5721],{},"aria-busy=\"true\""," mirroring the loading state, and ",[14,5724,5725],{},"aria-live=\"polite\""," so screen readers announce the loading state without interrupting the user. A visually-hidden ",[14,5728,5729],{},"\u003Cspan class=\"a-skel-sr-only\">Loading...\u003C\u002Fspan>"," ships with every variant so the announcement has text to read.",[10,5732,5733,5734,5737,5738,5264,5741,5744,5745,5748],{},"Every emitted shimmer surface carries ",[14,5735,5736],{},"aria-hidden=\"true\"",": the placeholder is decorative; the wrapper is what gets announced. Mirror-mode skeletons disable ",[14,5739,5740],{},"user-select",[14,5742,5743],{},"pointer-events"," on the slot tree so the placeholder can't be interacted with mid-load. ",[14,5746,5747],{},"prefers-reduced-motion"," strips animation.",[10,5750,5751,5752,5755],{},"None of this is configurable. None of it should be. The defaults are the right answer. If you need them off, you're either testing something specific (in which case ",[14,5753,5754],{},"animation=\"none\""," is the lever) or doing something a11y-wise that I don't want to make easy.",[29,5757,5759],{"id":5758},"what-clone-mode-taught-me","What clone mode taught me",[10,5761,5762,5763,5766,5767,5770],{},"The thing I'd carry into the next problem isn't the API. It's the insight that powers clone mode. You can stop reasoning about the cascade. You can stop trying to predict what ",[14,5764,5765],{},"border-radius: 0.75rem"," mixed with ",[14,5768,5769],{},"rounded-2xl"," mixed with a scoped style override actually produces. Mount the thing. Let the browser collapse all the inputs. Read the final answer. Replay the final answer.",[10,5772,5773,5774,5776],{},"That principle applies anywhere you have a copy-of-this-element problem. Print previews. Drag previews. Component galleries that render the same widget under five themes. The \"look at what's actually on the screen, not at the rules that produced it\" approach scales further than people give it credit for. Every CSS pipeline, every variable, every cascade priority, every animation frame ends up resolved to one number per property per element, and that number is what ",[14,5775,4241],{}," will tell you if you ask.",[10,5778,5779],{},"The other thing that earned its keep is the authoring contract. Tags unconditional, content via interpolation. Once you internalise that, every component you build is automatically skeleton-ready: no extra template, no parallel state, no maintenance overhead. The library reads your real component; you keep writing it the way you already wanted to.",[10,5781,5782],{},"Two ideas, one package. The repo is MIT-licensed and the README has the long-form reference for every prop and slot if you want to dig further. If you spot something that could be sharper, open an issue. I'd genuinely rather be corrected than comfortable.",[4010,5784,5785],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}",{"title":45,"searchDepth":58,"depth":58,"links":5787},[5788,5789,5790,5791,5792,5793,5794,5795,5796],{"id":4077,"depth":58,"text":4078},{"id":4219,"depth":58,"text":4220},{"id":4434,"depth":58,"text":4435},{"id":4524,"depth":58,"text":4525},{"id":5175,"depth":58,"text":5176},{"id":5270,"depth":58,"text":5271},{"id":5421,"depth":58,"text":5422},{"id":5710,"depth":58,"text":5711},{"id":5758,"depth":58,"text":5759},"2026-06-13","How a-skeleton derives placeholder UI from the real component. Clone mode, structural mode, mirror mode, and the capture, cache, and replay pipeline behind them.","كيف تستخرج a-skeleton واجهة الانتظار من المكوّن نفسه. وضع Clone، ووضع Structural، ووضع Mirror، ومسار العمل الذي يلتقط ثم يُخزّن مؤقّتاً ثم يُعيد العرض خلفها.",[5801,4049,5802,5803,5804,5805,5194,5806,5807,5808],"Vue 3","skeleton loader","loading states","getComputedStyle","Range API","TypeScript","component library","accessibility",{},"\u002Fwrites\u002Finside-a-skeleton-self-generating-skeleton-loaders",{"title":4060,"description":5798},"writes\u002Finside-a-skeleton-self-generating-skeleton-loaders","داخل @alikhalilll\u002Fa-skeleton، مُحمِّلات skeleton تولّد نفسها بنفسها","VxGF-EgIL4tFkWPV2BLDCQVNC3i8p1quakbSMMxOdpw",{"id":5816,"title":5817,"body":5818,"date":10296,"description":10297,"description_ar":10298,"draft":4039,"extension":4040,"image":4041,"keywords":10299,"lang":4050,"meta":10307,"navigation":115,"path":10308,"seo":10309,"stem":10310,"title_ar":10311,"updatedAt":10296,"__hash__":10312},"writes\u002Fwrites\u002Fatell-input-the-library-rewrite.md","Notes from rewriting ATellInput as a published library",{"type":7,"value":5819,"toc":10286},[5820,5826,5829,5836,5839,5843,5846,5906,5920,5923,5939,5942,6029,6055,6193,6209,6220,6223,6227,6230,6318,6328,6347,6353,6901,6904,6922,6949,6964,6967,6970,7144,7160,7230,7233,7237,7244,7250,7580,7583,7650,7653,7974,8000,8006,8049,8060,8063,8146,8155,8159,8162,8169,8280,8298,8301,8305,8308,8311,8875,8916,8923,9063,9076,9079,9209,9216,9219,9353,9367,9370,9374,9401,9404,9635,9642,9867,9870,10043,10050,10053,10057,10063,10068,10188,10198,10201,10205,10216,10223,10226,10269,10272,10283],[10,5821,5822,5825],{},[14,5823,5824],{},"ATellInput"," started its life inside one app: a Saudi\u002FEgypt-leaning fallback, an internal RestCountries fetch, validation surfaced via v-model only, a dropdown that was always on screen. That version solved correctness: cursor jumps, validation reasons, bidi. It got most things right for one consumer.",[10,5827,5828],{},"This post is what happened when I tried to publish it.",[10,5830,5831,5832,5835],{},"The component now lives in ",[14,5833,5834],{},"@alikhalilll\u002Fui\u002Ftell-input",", and a published library has a different job description. You can't bake in your own market. You can't assume the consumer's design system. You can't expect every mount point to wait for a third-party API. You don't get to decide whether the user is on a phone or a laptop. The rewrite kept the original's correctness and added what a library consumer actually needs, and almost every change has a specific bug or limitation in the old version that motivated it.",[10,5837,5838],{},"I've grouped the changes around the seven that earned their place.",[29,5840,5842],{"id":5841},"two-models-two-shapes-number-out-iso2-inside","Two models, two shapes: number out, ISO2 inside",[10,5844,5845],{},"The old surface looked clean from a distance:",[40,5847,5849],{"className":186,"code":5848,"language":188,"meta":45,"style":45},"const phoneModelValue = defineModel\u003Cstring>('phone', { required: true });\nconst countryModelValue = defineModel\u003Cstring>('country', { required: true });\n",[14,5850,5851,5880],{"__ignoreMap":45},[49,5852,5853,5855,5858,5860,5863,5865,5867,5869,5872,5875,5877],{"class":51,"line":52},[49,5854,951],{"class":195},[49,5856,5857],{"class":216}," phoneModelValue",[49,5859,213],{"class":195},[49,5861,5862],{"class":202}," defineModel",[49,5864,207],{"class":206},[49,5866,280],{"class":216},[49,5868,4682],{"class":206},[49,5870,5871],{"class":310},"'phone'",[49,5873,5874],{"class":206},", { required: ",[49,5876,3492],{"class":216},[49,5878,5879],{"class":206}," });\n",[49,5881,5882,5884,5887,5889,5891,5893,5895,5897,5900,5902,5904],{"class":51,"line":58},[49,5883,951],{"class":195},[49,5885,5886],{"class":216}," countryModelValue",[49,5888,213],{"class":195},[49,5890,5862],{"class":202},[49,5892,207],{"class":206},[49,5894,280],{"class":216},[49,5896,4682],{"class":206},[49,5898,5899],{"class":310},"'country'",[49,5901,5874],{"class":206},[49,5903,3492],{"class":216},[49,5905,5879],{"class":206},[10,5907,5908,5911,5912,5915,5916,5919],{},[14,5909,5910],{},"country"," was a string of dial digits: ",[14,5913,5914],{},"\"20\""," for Egypt, ",[14,5917,5918],{},"\"44\""," for the UK. Easy to serialize, easy to URL-encode, easy to put in a database column.",[10,5921,5922],{},"It also couldn't tell US from Canada.",[10,5924,5925,5928,5929,5932,5933,5935,5936,5938],{},[14,5926,5927],{},"+1"," is shared by 25+ NANP countries. If a Canadian user picked Canada in the dropdown and the v-model emitted ",[14,5930,5931],{},"\"1\"",", the parent had no way to know which ",[14,5934,5927],{}," was meant. Reload the page, hydrate from the URL, and you'd get back ",[14,5937,5931],{}," with the picker showing whichever country the dropdown happened to list first. The string-of-dial-digits model was a lossy serialization of a two-piece thing.",[10,5940,5941],{},"The new model splits it:",[40,5943,5945],{"className":186,"code":5944,"language":188,"meta":45,"style":45},"const phone = defineModel\u003Cstring>('phone', { default: '' });\nconst country = defineModel\u003Cnumber | null>('country', { default: null });\n\nconst selectedIso2 = ref\u003Cstring>('');\n",[14,5946,5947,5974,6004,6008],{"__ignoreMap":45},[49,5948,5949,5951,5954,5956,5958,5960,5962,5964,5966,5969,5972],{"class":51,"line":52},[49,5950,951],{"class":195},[49,5952,5953],{"class":216}," phone",[49,5955,213],{"class":195},[49,5957,5862],{"class":202},[49,5959,207],{"class":206},[49,5961,280],{"class":216},[49,5963,4682],{"class":206},[49,5965,5871],{"class":310},[49,5967,5968],{"class":206},", { default: ",[49,5970,5971],{"class":310},"''",[49,5973,5879],{"class":206},[49,5975,5976,5978,5981,5983,5985,5987,5990,5992,5994,5996,5998,6000,6002],{"class":51,"line":58},[49,5977,951],{"class":195},[49,5979,5980],{"class":216}," country",[49,5982,213],{"class":195},[49,5984,5862],{"class":202},[49,5986,207],{"class":206},[49,5988,5989],{"class":216},"number",[49,5991,235],{"class":195},[49,5993,4657],{"class":216},[49,5995,4682],{"class":206},[49,5997,5899],{"class":310},[49,5999,5968],{"class":206},[49,6001,4198],{"class":216},[49,6003,5879],{"class":206},[49,6005,6006],{"class":51,"line":64},[49,6007,116],{"emptyLinePlaceholder":115},[49,6009,6010,6012,6015,6017,6019,6021,6023,6025,6027],{"class":51,"line":70},[49,6011,951],{"class":195},[49,6013,6014],{"class":216}," selectedIso2",[49,6016,213],{"class":195},[49,6018,4628],{"class":202},[49,6020,207],{"class":206},[49,6022,280],{"class":216},[49,6024,4682],{"class":206},[49,6026,5971],{"class":310},[49,6028,1970],{"class":206},[10,6030,6031,6033,6034,283,6037,283,6040,6042,6043,6046,6047,6050,6051,6054],{},[14,6032,5910],{}," is a number now (",[14,6035,6036],{},"20",[14,6038,6039],{},"1",[14,6041,4198],{},") because dial codes ",[503,6044,6045],{},"are"," numbers, and ",[14,6048,6049],{},"Number"," survives URL\u002FDB\u002FJSON round-trips with no string-trimming weirdness. But the component also tracks an internal ",[14,6052,6053],{},"selectedIso2"," that the picker owns. The two stay in sync via a pair of watchers, one outward, one inward, and the inward one has a small but important guard:",[40,6056,6058],{"className":186,"code":6057,"language":188,"meta":45,"style":45},"watch(\n  country,\n  (next) => {\n    if (next == null) {\n      if (selectedIso2.value) selectedIso2.value = '';\n      return;\n    }\n    if (dialNumberFor(selectedIso2.value) === next) return;\n    const iso2 = resolveCountryIdentifier(String(next));\n    if (iso2) selectedIso2.value = iso2;\n  },\n  { immediate: true }\n);\n",[14,6059,6060,6066,6071,6085,6099,6114,6120,6124,6145,6164,6176,6180,6189],{"__ignoreMap":45},[49,6061,6062,6064],{"class":51,"line":52},[49,6063,3596],{"class":202},[49,6065,978],{"class":206},[49,6067,6068],{"class":51,"line":58},[49,6069,6070],{"class":206},"  country,\n",[49,6072,6073,6076,6079,6081,6083],{"class":51,"line":64},[49,6074,6075],{"class":206},"  (",[49,6077,6078],{"class":225},"next",[49,6080,439],{"class":206},[49,6082,361],{"class":195},[49,6084,967],{"class":206},[49,6086,6087,6089,6092,6095,6097],{"class":51,"line":70},[49,6088,1698],{"class":195},[49,6090,6091],{"class":206}," (next ",[49,6093,6094],{"class":195},"==",[49,6096,4657],{"class":216},[49,6098,1717],{"class":206},[49,6100,6101,6104,6107,6109,6112],{"class":51,"line":76},[49,6102,6103],{"class":195},"      if",[49,6105,6106],{"class":206}," (selectedIso2.value) selectedIso2.value ",[49,6108,638],{"class":195},[49,6110,6111],{"class":310}," ''",[49,6113,324],{"class":206},[49,6115,6116,6118],{"class":51,"line":82},[49,6117,1722],{"class":195},[49,6119,324],{"class":206},[49,6121,6122],{"class":51,"line":88},[49,6123,1744],{"class":206},[49,6125,6126,6128,6130,6133,6136,6138,6141,6143],{"class":51,"line":94},[49,6127,1698],{"class":195},[49,6129,419],{"class":206},[49,6131,6132],{"class":202},"dialNumberFor",[49,6134,6135],{"class":206},"(selectedIso2.value) ",[49,6137,1296],{"class":195},[49,6139,6140],{"class":206}," next) ",[49,6142,1280],{"class":195},[49,6144,324],{"class":206},[49,6146,6147,6149,6152,6154,6157,6159,6161],{"class":51,"line":100},[49,6148,3223],{"class":195},[49,6150,6151],{"class":216}," iso2",[49,6153,213],{"class":195},[49,6155,6156],{"class":202}," resolveCountryIdentifier",[49,6158,1072],{"class":206},[49,6160,2033],{"class":202},[49,6162,6163],{"class":206},"(next));\n",[49,6165,6166,6168,6171,6173],{"class":51,"line":106},[49,6167,1698],{"class":195},[49,6169,6170],{"class":206}," (iso2) selectedIso2.value ",[49,6172,638],{"class":195},[49,6174,6175],{"class":206}," iso2;\n",[49,6177,6178],{"class":51,"line":112},[49,6179,3645],{"class":206},[49,6181,6182,6185,6187],{"class":51,"line":119},[49,6183,6184],{"class":206},"  { immediate: ",[49,6186,3492],{"class":216},[49,6188,3501],{"class":206},[49,6190,6191],{"class":51,"line":125},[49,6192,1970],{"class":206},[10,6194,891,6195,6198,6199,6202,6203,6205,6206,6208],{},[14,6196,6197],{},"if (dialNumberFor(selectedIso2.value) === next) return;"," line is the one I want to call out. When the parent writes ",[14,6200,6201],{},"country = 1"," back to the model and the picker already has Canada selected, the watch fires, but the dial code already matches, so the watch returns without touching ",[14,6204,6053],{},".\nWithout that guard, the watcher would resolve ",[14,6207,5931],{}," to its alphabetically-first NANP country and silently flip Canada to whatever that is. The guard preserves the richer state.",[10,6210,6211,6212,6215,6216,6219],{},"The outward watch is ",[14,6213,6214],{},"flush: 'sync'"," so a separate ",[14,6217,6218],{},"autoSettingCountry"," flag stays consistent. When the matcher auto-picks a country from typed input, we don't want it counted as a \"manual pick\" that would lock out further auto-detection.",[10,6221,6222],{},"Two halves of the country, two distinct models, one source of truth for each.",[29,6224,6226],{"id":6225},"detection-first-the-picker-is-hidden-by-default","Detection-first: the picker is hidden by default",[10,6228,6229],{},"The old component always rendered the dropdown: flag on the left, input on the right. The new component defaults to the input alone. The dropdown slides in only when the user types something the matcher recognises:",[40,6231,6233],{"className":587,"code":6232,"language":589,"meta":45,"style":45},"\u003CTransition\n  enter-active-class=\"transition-all duration-200 ease-out overflow-hidden\"\n  leave-active-class=\"transition-all duration-150 ease-in overflow-hidden\"\n  enter-from-class=\"opacity-0 -translate-x-1 max-w-0\"\n  leave-to-class=\"opacity-0 -translate-x-1 max-w-0\"\n  enter-to-class=\"max-w-[12rem]\"\n  leave-from-class=\"max-w-[12rem]\"\n>\n  \u003CACountrySelect v-if=\"!props.detectFromInput || selectedIso2\" ... \u002F>\n\u003C\u002FTransition>\n",[14,6234,6235,6242,6252,6262,6272,6281,6291,6300,6304,6309],{"__ignoreMap":45},[49,6236,6237,6239],{"class":51,"line":52},[49,6238,207],{"class":206},[49,6240,6241],{"class":598},"Transition\n",[49,6243,6244,6247,6249],{"class":51,"line":58},[49,6245,6246],{"class":202},"  enter-active-class",[49,6248,638],{"class":206},[49,6250,6251],{"class":310},"\"transition-all duration-200 ease-out overflow-hidden\"\n",[49,6253,6254,6257,6259],{"class":51,"line":64},[49,6255,6256],{"class":202},"  leave-active-class",[49,6258,638],{"class":206},[49,6260,6261],{"class":310},"\"transition-all duration-150 ease-in overflow-hidden\"\n",[49,6263,6264,6267,6269],{"class":51,"line":70},[49,6265,6266],{"class":202},"  enter-from-class",[49,6268,638],{"class":206},[49,6270,6271],{"class":310},"\"opacity-0 -translate-x-1 max-w-0\"\n",[49,6273,6274,6277,6279],{"class":51,"line":76},[49,6275,6276],{"class":202},"  leave-to-class",[49,6278,638],{"class":206},[49,6280,6271],{"class":310},[49,6282,6283,6286,6288],{"class":51,"line":82},[49,6284,6285],{"class":202},"  enter-to-class",[49,6287,638],{"class":206},[49,6289,6290],{"class":310},"\"max-w-[12rem]\"\n",[49,6292,6293,6296,6298],{"class":51,"line":88},[49,6294,6295],{"class":202},"  leave-from-class",[49,6297,638],{"class":206},[49,6299,6290],{"class":310},[49,6301,6302],{"class":51,"line":94},[49,6303,602],{"class":206},[49,6305,6306],{"class":51,"line":100},[49,6307,6308],{"class":206},"  \u003CACountrySelect v-if=\"!props.detectFromInput || selectedIso2\" ... \u002F>\n",[49,6310,6311,6313,6316],{"class":51,"line":106},[49,6312,738],{"class":206},[49,6314,6315],{"class":598},"Transition",[49,6317,602],{"class":206},[10,6319,6320,6323,6324,6327],{},[14,6321,6322],{},"max-w"," collapses to zero when nothing is selected, animates open on the first successful match. ",[14,6325,6326],{},"detectFromInput=\"false\""," brings the always-visible picker back for callers who want the legacy shape.",[10,6329,6330,6331,6334,6335,6338,6339,6342,6343,6346],{},"The behaviour is opinionated. Most users in most countries ",[503,6332,6333],{},"never"," type a ",[14,6336,6337],{},"+"," prefix. They type the number they would type into their own phone: ",[14,6340,6341],{},"01066105963"," in Cairo, ",[14,6344,6345],{},"07911 123456"," in Manchester. The dropdown they \"have to interact with\" in every other phone input is, for those users, a step they don't need. Hide it. Detect what they're doing. Show it only when the detection lands.",[10,6348,6349,6350,229],{},"The \"detect what they're doing\" part is the only non-trivial piece. It runs through three tiers in ",[14,6351,6352],{},"matchLeadingDialCode",[40,6354,6356],{"className":186,"code":6355,"language":188,"meta":45,"style":45},"function matchLeadingDialCode(digits: string): DialMatch | null {\n  if (!digits) return null;\n\n  \u002F\u002F Tier 1: international parse, disambiguates NANP and strips the calling code.\n  try {\n    const parsed = parsePhoneNumberFromString(`+${digits}`);\n    if (parsed?.country && parsed.countryCallingCode) {\n      const parsedCountry = getCountryByValue(parsed.country);\n      if (parsedCountry) {\n        return { country: parsedCountry, nationalNumber: String(parsed.nationalNumber ?? '') };\n      }\n    }\n  } catch {\n    \u002F* libphonenumber throws on partial input, fall through *\u002F\n  }\n\n  \u002F\u002F Tier 2: national-format parse using the silently-inferred country as a hint.\n  const hint = inferredCountry.value;\n  if (hint && digits.length >= 4) {\n    try {\n      const parsed = parsePhoneNumberFromString(digits, hint as CountryCode);\n      if (parsed?.isValid()) {\n        const matched = getCountryByValue(parsed.country || hint);\n        if (matched) {\n          return { country: matched, nationalNumber: String(parsed.nationalNumber ?? '') };\n        }\n      }\n    } catch {\n      \u002F* fall through *\u002F\n    }\n  }\n\n  \u002F\u002F Tier 3: longest-prefix match over our own dial-digits index.\n  for (let len = Math.min(3, digits.length); len >= 1; len--) {\n    const prefix = digits.slice(0, len);\n    const group = getCountriesByDial(prefix);\n    if (!group.length) continue;\n    const nationalNumber = digits.slice(prefix.length);\n    if (group.length === 1) return { country: group[0], nationalNumber };\n    \u002F\u002F ambiguity: prefer current selection, then recents, then first\n    \u002F\u002F (see \"Recents as the tiebreaker\" below)\n    return { country: group[0], nationalNumber };\n  }\n  return null;\n}\n",[14,6357,6358,6387,6404,6408,6413,6420,6443,6455,6471,6478,6499,6504,6508,6517,6522,6526,6530,6535,6547,6569,6576,6596,6609,6630,6638,6656,6662,6667,6677,6683,6688,6693,6698,6704,6752,6775,6791,6812,6833,6859,6865,6871,6882,6887,6896],{"__ignoreMap":45},[49,6359,6360,6362,6365,6367,6370,6372,6374,6376,6378,6381,6383,6385],{"class":51,"line":52},[49,6361,1894],{"class":195},[49,6363,6364],{"class":202}," matchLeadingDialCode",[49,6366,1072],{"class":206},[49,6368,6369],{"class":225},"digits",[49,6371,229],{"class":195},[49,6373,232],{"class":216},[49,6375,1642],{"class":206},[49,6377,229],{"class":195},[49,6379,6380],{"class":202}," DialMatch",[49,6382,235],{"class":195},[49,6384,4657],{"class":216},[49,6386,967],{"class":206},[49,6388,6389,6391,6393,6395,6398,6400,6402],{"class":51,"line":58},[49,6390,1269],{"class":195},[49,6392,419],{"class":206},[49,6394,1274],{"class":195},[49,6396,6397],{"class":206},"digits) ",[49,6399,1280],{"class":195},[49,6401,4657],{"class":216},[49,6403,324],{"class":206},[49,6405,6406],{"class":51,"line":64},[49,6407,116],{"emptyLinePlaceholder":115},[49,6409,6410],{"class":51,"line":70},[49,6411,6412],{"class":1852},"  \u002F\u002F Tier 1: international parse, disambiguates NANP and strips the calling code.\n",[49,6414,6415,6418],{"class":51,"line":76},[49,6416,6417],{"class":195},"  try",[49,6419,967],{"class":206},[49,6421,6422,6424,6427,6429,6432,6434,6437,6439,6441],{"class":51,"line":82},[49,6423,3223],{"class":195},[49,6425,6426],{"class":216}," parsed",[49,6428,213],{"class":195},[49,6430,6431],{"class":202}," parsePhoneNumberFromString",[49,6433,1072],{"class":206},[49,6435,6436],{"class":310},"`+${",[49,6438,6369],{"class":206},[49,6440,2048],{"class":310},[49,6442,1970],{"class":206},[49,6444,6445,6447,6450,6452],{"class":51,"line":88},[49,6446,1698],{"class":195},[49,6448,6449],{"class":206}," (parsed?.country ",[49,6451,1704],{"class":195},[49,6453,6454],{"class":206}," parsed.countryCallingCode) {\n",[49,6456,6457,6460,6463,6465,6468],{"class":51,"line":94},[49,6458,6459],{"class":195},"      const",[49,6461,6462],{"class":216}," parsedCountry",[49,6464,213],{"class":195},[49,6466,6467],{"class":202}," getCountryByValue",[49,6469,6470],{"class":206},"(parsed.country);\n",[49,6472,6473,6475],{"class":51,"line":100},[49,6474,6103],{"class":195},[49,6476,6477],{"class":206}," (parsedCountry) {\n",[49,6479,6480,6483,6486,6488,6491,6494,6496],{"class":51,"line":106},[49,6481,6482],{"class":195},"        return",[49,6484,6485],{"class":206}," { country: parsedCountry, nationalNumber: ",[49,6487,2033],{"class":202},[49,6489,6490],{"class":206},"(parsed.nationalNumber ",[49,6492,6493],{"class":195},"??",[49,6495,6111],{"class":310},[49,6497,6498],{"class":206},") };\n",[49,6500,6501],{"class":51,"line":112},[49,6502,6503],{"class":206},"      }\n",[49,6505,6506],{"class":51,"line":119},[49,6507,1744],{"class":206},[49,6509,6510,6512,6515],{"class":51,"line":125},[49,6511,2942],{"class":206},[49,6513,6514],{"class":195},"catch",[49,6516,967],{"class":206},[49,6518,6519],{"class":51,"line":131},[49,6520,6521],{"class":1852},"    \u002F* libphonenumber throws on partial input, fall through *\u002F\n",[49,6523,6524],{"class":51,"line":137},[49,6525,109],{"class":206},[49,6527,6528],{"class":51,"line":143},[49,6529,116],{"emptyLinePlaceholder":115},[49,6531,6532],{"class":51,"line":149},[49,6533,6534],{"class":1852},"  \u002F\u002F Tier 2: national-format parse using the silently-inferred country as a hint.\n",[49,6536,6537,6539,6542,6544],{"class":51,"line":155},[49,6538,1438],{"class":195},[49,6540,6541],{"class":216}," hint",[49,6543,213],{"class":195},[49,6545,6546],{"class":206}," inferredCountry.value;\n",[49,6548,6549,6551,6554,6556,6559,6561,6564,6567],{"class":51,"line":161},[49,6550,1269],{"class":195},[49,6552,6553],{"class":206}," (hint ",[49,6555,1704],{"class":195},[49,6557,6558],{"class":206}," digits.",[49,6560,2412],{"class":216},[49,6562,6563],{"class":195}," >=",[49,6565,6566],{"class":216}," 4",[49,6568,1717],{"class":206},[49,6570,6571,6574],{"class":51,"line":166},[49,6572,6573],{"class":195},"    try",[49,6575,967],{"class":206},[49,6577,6578,6580,6582,6584,6586,6589,6591,6594],{"class":51,"line":3262},[49,6579,6459],{"class":195},[49,6581,6426],{"class":216},[49,6583,213],{"class":195},[49,6585,6431],{"class":202},[49,6587,6588],{"class":206},"(digits, hint ",[49,6590,1536],{"class":195},[49,6592,6593],{"class":202}," CountryCode",[49,6595,1970],{"class":206},[49,6597,6598,6600,6603,6606],{"class":51,"line":3274},[49,6599,6103],{"class":195},[49,6601,6602],{"class":206}," (parsed?.",[49,6604,6605],{"class":202},"isValid",[49,6607,6608],{"class":206},"()) {\n",[49,6610,6611,6614,6617,6619,6621,6624,6627],{"class":51,"line":4880},[49,6612,6613],{"class":195},"        const",[49,6615,6616],{"class":216}," matched",[49,6618,213],{"class":195},[49,6620,6467],{"class":202},[49,6622,6623],{"class":206},"(parsed.country ",[49,6625,6626],{"class":195},"||",[49,6628,6629],{"class":206}," hint);\n",[49,6631,6632,6635],{"class":51,"line":4901},[49,6633,6634],{"class":195},"        if",[49,6636,6637],{"class":206}," (matched) {\n",[49,6639,6640,6643,6646,6648,6650,6652,6654],{"class":51,"line":4910},[49,6641,6642],{"class":195},"          return",[49,6644,6645],{"class":206}," { country: matched, nationalNumber: ",[49,6647,2033],{"class":202},[49,6649,6490],{"class":206},[49,6651,6493],{"class":195},[49,6653,6111],{"class":310},[49,6655,6498],{"class":206},[49,6657,6659],{"class":51,"line":6658},26,[49,6660,6661],{"class":206},"        }\n",[49,6663,6665],{"class":51,"line":6664},27,[49,6666,6503],{"class":206},[49,6668,6670,6673,6675],{"class":51,"line":6669},28,[49,6671,6672],{"class":206},"    } ",[49,6674,6514],{"class":195},[49,6676,967],{"class":206},[49,6678,6680],{"class":51,"line":6679},29,[49,6681,6682],{"class":1852},"      \u002F* fall through *\u002F\n",[49,6684,6686],{"class":51,"line":6685},30,[49,6687,1744],{"class":206},[49,6689,6691],{"class":51,"line":6690},31,[49,6692,109],{"class":206},[49,6694,6696],{"class":51,"line":6695},32,[49,6697,116],{"emptyLinePlaceholder":115},[49,6699,6701],{"class":51,"line":6700},33,[49,6702,6703],{"class":1852},"  \u002F\u002F Tier 3: longest-prefix match over our own dial-digits index.\n",[49,6705,6707,6710,6712,6715,6718,6720,6723,6726,6728,6730,6733,6735,6738,6741,6744,6747,6750],{"class":51,"line":6706},34,[49,6708,6709],{"class":195},"  for",[49,6711,419],{"class":206},[49,6713,6714],{"class":195},"let",[49,6716,6717],{"class":206}," len ",[49,6719,638],{"class":195},[49,6721,6722],{"class":206}," Math.",[49,6724,6725],{"class":202},"min",[49,6727,1072],{"class":206},[49,6729,887],{"class":216},[49,6731,6732],{"class":206},", digits.",[49,6734,2412],{"class":216},[49,6736,6737],{"class":206},"); len ",[49,6739,6740],{"class":195},">=",[49,6742,6743],{"class":216}," 1",[49,6745,6746],{"class":206},"; len",[49,6748,6749],{"class":195},"--",[49,6751,1717],{"class":206},[49,6753,6755,6757,6760,6762,6764,6767,6769,6772],{"class":51,"line":6754},35,[49,6756,3223],{"class":195},[49,6758,6759],{"class":216}," prefix",[49,6761,213],{"class":195},[49,6763,6558],{"class":206},[49,6765,6766],{"class":202},"slice",[49,6768,1072],{"class":206},[49,6770,6771],{"class":216},"0",[49,6773,6774],{"class":206},", len);\n",[49,6776,6778,6780,6783,6785,6788],{"class":51,"line":6777},36,[49,6779,3223],{"class":195},[49,6781,6782],{"class":216}," group",[49,6784,213],{"class":195},[49,6786,6787],{"class":202}," getCountriesByDial",[49,6789,6790],{"class":206},"(prefix);\n",[49,6792,6794,6796,6798,6800,6803,6805,6807,6810],{"class":51,"line":6793},37,[49,6795,1698],{"class":195},[49,6797,419],{"class":206},[49,6799,1274],{"class":195},[49,6801,6802],{"class":206},"group.",[49,6804,2412],{"class":216},[49,6806,439],{"class":206},[49,6808,6809],{"class":195},"continue",[49,6811,324],{"class":206},[49,6813,6815,6817,6820,6822,6824,6826,6829,6831],{"class":51,"line":6814},38,[49,6816,3223],{"class":195},[49,6818,6819],{"class":216}," nationalNumber",[49,6821,213],{"class":195},[49,6823,6558],{"class":206},[49,6825,6766],{"class":202},[49,6827,6828],{"class":206},"(prefix.",[49,6830,2412],{"class":216},[49,6832,1970],{"class":206},[49,6834,6836,6838,6841,6843,6845,6847,6849,6851,6854,6856],{"class":51,"line":6835},39,[49,6837,1698],{"class":195},[49,6839,6840],{"class":206}," (group.",[49,6842,2412],{"class":216},[49,6844,2415],{"class":195},[49,6846,6743],{"class":216},[49,6848,439],{"class":206},[49,6850,1280],{"class":195},[49,6852,6853],{"class":206}," { country: group[",[49,6855,6771],{"class":216},[49,6857,6858],{"class":206},"], nationalNumber };\n",[49,6860,6862],{"class":51,"line":6861},40,[49,6863,6864],{"class":1852},"    \u002F\u002F ambiguity: prefer current selection, then recents, then first\n",[49,6866,6868],{"class":51,"line":6867},41,[49,6869,6870],{"class":1852},"    \u002F\u002F (see \"Recents as the tiebreaker\" below)\n",[49,6872,6874,6876,6878,6880],{"class":51,"line":6873},42,[49,6875,1557],{"class":195},[49,6877,6853],{"class":206},[49,6879,6771],{"class":216},[49,6881,6858],{"class":206},[49,6883,6885],{"class":51,"line":6884},43,[49,6886,109],{"class":206},[49,6888,6890,6892,6894],{"class":51,"line":6889},44,[49,6891,972],{"class":195},[49,6893,4657],{"class":216},[49,6895,324],{"class":206},[49,6897,6899],{"class":51,"line":6898},45,[49,6900,169],{"class":206},[10,6902,6903],{},"The tiers exist because each handles a class of input the other tiers can't.",[10,6905,6906,6907,6909,6910,6913,6914,6917,6918,6921],{},"Tier 1 takes whatever the user typed, prepends ",[14,6908,6337],{},", and asks libphonenumber to parse it as an international number. If the user typed ",[14,6911,6912],{},"447911123456",", this matches the UK. If they typed ",[14,6915,6916],{},"1416...",", it matches Canada specifically (not generic NANP), because libphonenumber has area-code rules baked in. The catch block exists because libphonenumber throws on partial input. ",[14,6919,6920],{},"447"," isn't a complete number; parsing it raises, and we fall through.",[10,6923,6924,6925,6927,6928,6931,6932,6935,6936,6938,6939,6941,6942,755,6945,6948],{},"Tier 2 is the path for users who type the number they actually use locally. ",[14,6926,6341],{}," doesn't start with a dial code, so Tier 1 can't help. But if we ",[503,6929,6930],{},"silently"," know the user is probably in Egypt (from IP or timezone; more on that next), we can pass ",[14,6933,6934],{},"\"EG\""," as a parsing hint and libphonenumber will recognise ",[14,6937,6341],{}," as a valid Egyptian mobile number, strip the leading ",[14,6940,6771],{}," (Egypt's national prefix), and hand back the canonical national significant number ",[14,6943,6944],{},"1066105963",[14,6946,6947],{},"digits.length >= 4"," guard prevents the parser from treating two-digit area-code starts as full numbers.",[10,6950,6951,6952,6955,6956,6959,6960,6963],{},"Tier 3 is the fallback for \"you typed ",[14,6953,6954],{},"44"," and nothing else.\" Neither libphonenumber pass can match; the input is too short. But our own dial-digits index can: ",[14,6957,6958],{},"byDialDigits.get('44')"," returns the UK group, length 1, done. The bucketed-",[14,6961,6962],{},"Map\u003Cstring, CountryOption[]>"," shape from the old version earns its keep here.",[10,6965,6966],{},"Three tiers feels like a lot. It's the minimum: each tier handles inputs the other two reject.",[10,6968,6969],{},"The handler that calls into it stays small:",[40,6971,6973],{"className":186,"code":6972,"language":188,"meta":45,"style":45},"const detectAndApply = useDebounceFn(\n  () => {\n    if (!props.detectFromInput) return;\n    if (userPickedCountry.value || selectedIso2.value) return;\n    const current = phone.value;\n    if (!current) return;\n    const match = matchLeadingDialCode(current);\n    if (!match) return;\n    autoSettingCountry.value = true;\n    selectedIso2.value = match.country.value;\n    phone.value = match.nationalNumber;\n  },\n  computed(() => Math.max(0, props.detectDebounceMs))\n);\n",[14,6974,6975,6989,6997,7012,7028,7040,7055,7069,7084,7095,7105,7115,7119,7140],{"__ignoreMap":45},[49,6976,6977,6979,6982,6984,6987],{"class":51,"line":52},[49,6978,951],{"class":195},[49,6980,6981],{"class":216}," detectAndApply",[49,6983,213],{"class":195},[49,6985,6986],{"class":202}," useDebounceFn",[49,6988,978],{"class":206},[49,6990,6991,6993,6995],{"class":51,"line":58},[49,6992,3461],{"class":206},[49,6994,361],{"class":195},[49,6996,967],{"class":206},[49,6998,6999,7001,7003,7005,7008,7010],{"class":51,"line":64},[49,7000,1698],{"class":195},[49,7002,419],{"class":206},[49,7004,1274],{"class":195},[49,7006,7007],{"class":206},"props.detectFromInput) ",[49,7009,1280],{"class":195},[49,7011,324],{"class":206},[49,7013,7014,7016,7019,7021,7024,7026],{"class":51,"line":70},[49,7015,1698],{"class":195},[49,7017,7018],{"class":206}," (userPickedCountry.value ",[49,7020,6626],{"class":195},[49,7022,7023],{"class":206}," selectedIso2.value) ",[49,7025,1280],{"class":195},[49,7027,324],{"class":206},[49,7029,7030,7032,7035,7037],{"class":51,"line":76},[49,7031,3223],{"class":195},[49,7033,7034],{"class":216}," current",[49,7036,213],{"class":195},[49,7038,7039],{"class":206}," phone.value;\n",[49,7041,7042,7044,7046,7048,7051,7053],{"class":51,"line":82},[49,7043,1698],{"class":195},[49,7045,419],{"class":206},[49,7047,1274],{"class":195},[49,7049,7050],{"class":206},"current) ",[49,7052,1280],{"class":195},[49,7054,324],{"class":206},[49,7056,7057,7059,7062,7064,7066],{"class":51,"line":88},[49,7058,3223],{"class":195},[49,7060,7061],{"class":216}," match",[49,7063,213],{"class":195},[49,7065,6364],{"class":202},[49,7067,7068],{"class":206},"(current);\n",[49,7070,7071,7073,7075,7077,7080,7082],{"class":51,"line":94},[49,7072,1698],{"class":195},[49,7074,419],{"class":206},[49,7076,1274],{"class":195},[49,7078,7079],{"class":206},"match) ",[49,7081,1280],{"class":195},[49,7083,324],{"class":206},[49,7085,7086,7089,7091,7093],{"class":51,"line":100},[49,7087,7088],{"class":206},"    autoSettingCountry.value ",[49,7090,638],{"class":195},[49,7092,2935],{"class":216},[49,7094,324],{"class":206},[49,7096,7097,7100,7102],{"class":51,"line":106},[49,7098,7099],{"class":206},"    selectedIso2.value ",[49,7101,638],{"class":195},[49,7103,7104],{"class":206}," match.country.value;\n",[49,7106,7107,7110,7112],{"class":51,"line":112},[49,7108,7109],{"class":206},"    phone.value ",[49,7111,638],{"class":195},[49,7113,7114],{"class":206}," match.nationalNumber;\n",[49,7116,7117],{"class":51,"line":119},[49,7118,3645],{"class":206},[49,7120,7121,7124,7126,7128,7130,7133,7135,7137],{"class":51,"line":125},[49,7122,7123],{"class":202},"  computed",[49,7125,962],{"class":206},[49,7127,361],{"class":195},[49,7129,6722],{"class":206},[49,7131,7132],{"class":202},"max",[49,7134,1072],{"class":206},[49,7136,6771],{"class":216},[49,7138,7139],{"class":206},", props.detectDebounceMs))\n",[49,7141,7142],{"class":51,"line":131},[49,7143,1970],{"class":206},[10,7145,7146,5203,7149,7152,7153,7156,7157,7159],{},[14,7147,7148],{},"useDebounceFn",[14,7150,7151],{},"phone.value"," at the time the timer fires, not at schedule time, so a burst of keystrokes collapses into one parse. ",[14,7154,7155],{},"userPickedCountry.value"," stops the auto-detector from clobbering a manual pick. Once the user has used the dropdown, the matcher steps aside. And clearing the input is ",[503,7158,3573],{}," debounced; it reverts the picker to hidden immediately:",[40,7161,7163],{"className":186,"code":7162,"language":188,"meta":45,"style":45},"if (!cleaned) {\n  autoSettingCountry.value = true;\n  selectedIso2.value = '';\n  phone.value = '';\n  userPickedCountry.value = false;\n  return;\n}\n",[14,7164,7165,7176,7187,7198,7209,7220,7226],{"__ignoreMap":45},[49,7166,7167,7169,7171,7173],{"class":51,"line":52},[49,7168,2487],{"class":195},[49,7170,419],{"class":206},[49,7172,1274],{"class":195},[49,7174,7175],{"class":206},"cleaned) {\n",[49,7177,7178,7181,7183,7185],{"class":51,"line":58},[49,7179,7180],{"class":206},"  autoSettingCountry.value ",[49,7182,638],{"class":195},[49,7184,2935],{"class":216},[49,7186,324],{"class":206},[49,7188,7189,7192,7194,7196],{"class":51,"line":64},[49,7190,7191],{"class":206},"  selectedIso2.value ",[49,7193,638],{"class":195},[49,7195,6111],{"class":310},[49,7197,324],{"class":206},[49,7199,7200,7203,7205,7207],{"class":51,"line":70},[49,7201,7202],{"class":206},"  phone.value ",[49,7204,638],{"class":195},[49,7206,6111],{"class":310},[49,7208,324],{"class":206},[49,7210,7211,7214,7216,7218],{"class":51,"line":76},[49,7212,7213],{"class":206},"  userPickedCountry.value ",[49,7215,638],{"class":195},[49,7217,2983],{"class":216},[49,7219,324],{"class":206},[49,7221,7222,7224],{"class":51,"line":82},[49,7223,972],{"class":195},[49,7225,324],{"class":206},[49,7227,7228],{"class":51,"line":88},[49,7229,169],{"class":206},[10,7231,7232],{},"Debouncing the clear would mean the dropdown lingers for 150ms after the input is empty, which looks wrong every time.",[29,7234,7236],{"id":7235},"the-environment-detection-chain-and-why-its-pluggable","The environment-detection chain (and why it's pluggable)",[10,7238,7239,7240,7243],{},"Tier 2 above depends on ",[14,7241,7242],{},"inferredCountry.value",": the component's best guess at where the user is, set silently before they type anything. The old version's \"best guess\" was a hardcoded fallback to Saudi Arabia or Egypt, because those were the markets the in-app version targeted. A library can't make that assumption.",[10,7245,7246,7247,229],{},"The new chain is ",[14,7248,7249],{},"detectCountry()",[40,7251,7253],{"className":186,"code":7252,"language":188,"meta":45,"style":45},"export async function detectCountry(opts: DetectCountryOptions = {}): Promise\u003Cstring> {\n  const {\n    strategy = 'auto',\n    ipEndpoint = 'https:\u002F\u002Fipapi.co\u002Fjson\u002F',\n    defaultCountry = 'US',\n    timeoutMs = 2000,\n    cache = true,\n  } = opts;\n\n  if (cache) {\n    const cached = readCache();\n    if (cached) return cached;\n  }\n\n  if (strategy === 'none') {\n    return defaultCountry.toUpperCase();\n  }\n\n  if (strategy === 'auto') {\n    const ipResult = await tryIp(ipEndpoint, timeoutMs);\n    if (ipResult) {\n      if (cache) writeCache(ipResult);\n      return ipResult;\n    }\n  }\n\n  const localResult = tryTimezone() ?? tryLocale();\n  const final = (localResult ?? defaultCountry).toUpperCase();\n  if (cache) writeCache(final);\n  return final;\n}\n",[14,7254,7255,7294,7300,7312,7324,7336,7348,7359,7368,7372,7379,7393,7405,7409,7413,7427,7439,7443,7447,7459,7477,7484,7497,7504,7508,7512,7516,7537,7558,7569,7576],{"__ignoreMap":45},[49,7256,7257,7259,7262,7265,7268,7270,7273,7275,7278,7280,7283,7285,7288,7290,7292],{"class":51,"line":52},[49,7258,196],{"class":195},[49,7260,7261],{"class":195}," async",[49,7263,7264],{"class":195}," function",[49,7266,7267],{"class":202}," detectCountry",[49,7269,1072],{"class":206},[49,7271,7272],{"class":225},"opts",[49,7274,229],{"class":195},[49,7276,7277],{"class":202}," DetectCountryOptions",[49,7279,213],{"class":195},[49,7281,7282],{"class":206}," {})",[49,7284,229],{"class":195},[49,7286,7287],{"class":202}," Promise",[49,7289,207],{"class":206},[49,7291,280],{"class":216},[49,7293,220],{"class":206},[49,7295,7296,7298],{"class":51,"line":58},[49,7297,1438],{"class":195},[49,7299,967],{"class":206},[49,7301,7302,7305,7307,7310],{"class":51,"line":64},[49,7303,7304],{"class":216},"    strategy",[49,7306,213],{"class":195},[49,7308,7309],{"class":310}," 'auto'",[49,7311,998],{"class":206},[49,7313,7314,7317,7319,7322],{"class":51,"line":70},[49,7315,7316],{"class":216},"    ipEndpoint",[49,7318,213],{"class":195},[49,7320,7321],{"class":310}," 'https:\u002F\u002Fipapi.co\u002Fjson\u002F'",[49,7323,998],{"class":206},[49,7325,7326,7329,7331,7334],{"class":51,"line":76},[49,7327,7328],{"class":216},"    defaultCountry",[49,7330,213],{"class":195},[49,7332,7333],{"class":310}," 'US'",[49,7335,998],{"class":206},[49,7337,7338,7341,7343,7346],{"class":51,"line":82},[49,7339,7340],{"class":216},"    timeoutMs",[49,7342,213],{"class":195},[49,7344,7345],{"class":216}," 2000",[49,7347,998],{"class":206},[49,7349,7350,7353,7355,7357],{"class":51,"line":88},[49,7351,7352],{"class":216},"    cache",[49,7354,213],{"class":195},[49,7356,2935],{"class":216},[49,7358,998],{"class":206},[49,7360,7361,7363,7365],{"class":51,"line":94},[49,7362,2942],{"class":206},[49,7364,638],{"class":195},[49,7366,7367],{"class":206}," opts;\n",[49,7369,7370],{"class":51,"line":100},[49,7371,116],{"emptyLinePlaceholder":115},[49,7373,7374,7376],{"class":51,"line":106},[49,7375,1269],{"class":195},[49,7377,7378],{"class":206}," (cache) {\n",[49,7380,7381,7383,7386,7388,7391],{"class":51,"line":112},[49,7382,3223],{"class":195},[49,7384,7385],{"class":216}," cached",[49,7387,213],{"class":195},[49,7389,7390],{"class":202}," readCache",[49,7392,1998],{"class":206},[49,7394,7395,7397,7400,7402],{"class":51,"line":119},[49,7396,1698],{"class":195},[49,7398,7399],{"class":206}," (cached) ",[49,7401,1280],{"class":195},[49,7403,7404],{"class":206}," cached;\n",[49,7406,7407],{"class":51,"line":125},[49,7408,109],{"class":206},[49,7410,7411],{"class":51,"line":131},[49,7412,116],{"emptyLinePlaceholder":115},[49,7414,7415,7417,7420,7422,7425],{"class":51,"line":137},[49,7416,1269],{"class":195},[49,7418,7419],{"class":206}," (strategy ",[49,7421,1296],{"class":195},[49,7423,7424],{"class":310}," 'none'",[49,7426,1717],{"class":206},[49,7428,7429,7431,7434,7437],{"class":51,"line":143},[49,7430,1557],{"class":195},[49,7432,7433],{"class":206}," defaultCountry.",[49,7435,7436],{"class":202},"toUpperCase",[49,7438,1998],{"class":206},[49,7440,7441],{"class":51,"line":149},[49,7442,109],{"class":206},[49,7444,7445],{"class":51,"line":155},[49,7446,116],{"emptyLinePlaceholder":115},[49,7448,7449,7451,7453,7455,7457],{"class":51,"line":161},[49,7450,1269],{"class":195},[49,7452,7419],{"class":206},[49,7454,1296],{"class":195},[49,7456,7309],{"class":310},[49,7458,1717],{"class":206},[49,7460,7461,7463,7466,7468,7471,7474],{"class":51,"line":166},[49,7462,3223],{"class":195},[49,7464,7465],{"class":216}," ipResult",[49,7467,213],{"class":195},[49,7469,7470],{"class":195}," await",[49,7472,7473],{"class":202}," tryIp",[49,7475,7476],{"class":206},"(ipEndpoint, timeoutMs);\n",[49,7478,7479,7481],{"class":51,"line":3262},[49,7480,1698],{"class":195},[49,7482,7483],{"class":206}," (ipResult) {\n",[49,7485,7486,7488,7491,7494],{"class":51,"line":3274},[49,7487,6103],{"class":195},[49,7489,7490],{"class":206}," (cache) ",[49,7492,7493],{"class":202},"writeCache",[49,7495,7496],{"class":206},"(ipResult);\n",[49,7498,7499,7501],{"class":51,"line":4880},[49,7500,1722],{"class":195},[49,7502,7503],{"class":206}," ipResult;\n",[49,7505,7506],{"class":51,"line":4901},[49,7507,1744],{"class":206},[49,7509,7510],{"class":51,"line":4910},[49,7511,109],{"class":206},[49,7513,7514],{"class":51,"line":6658},[49,7515,116],{"emptyLinePlaceholder":115},[49,7517,7518,7520,7523,7525,7528,7530,7532,7535],{"class":51,"line":6664},[49,7519,1438],{"class":195},[49,7521,7522],{"class":216}," localResult",[49,7524,213],{"class":195},[49,7526,7527],{"class":202}," tryTimezone",[49,7529,1569],{"class":206},[49,7531,6493],{"class":195},[49,7533,7534],{"class":202}," tryLocale",[49,7536,1998],{"class":206},[49,7538,7539,7541,7544,7546,7549,7551,7554,7556],{"class":51,"line":6669},[49,7540,1438],{"class":195},[49,7542,7543],{"class":216}," final",[49,7545,213],{"class":195},[49,7547,7548],{"class":206}," (localResult ",[49,7550,6493],{"class":195},[49,7552,7553],{"class":206}," defaultCountry).",[49,7555,7436],{"class":202},[49,7557,1998],{"class":206},[49,7559,7560,7562,7564,7566],{"class":51,"line":6679},[49,7561,1269],{"class":195},[49,7563,7490],{"class":206},[49,7565,7493],{"class":202},[49,7567,7568],{"class":206},"(final);\n",[49,7570,7571,7573],{"class":51,"line":6685},[49,7572,972],{"class":195},[49,7574,7575],{"class":206}," final;\n",[49,7577,7578],{"class":51,"line":6690},[49,7579,169],{"class":206},[10,7581,7582],{},"Four signals, evaluated in cost order: cheap-and-cached first, expensive-and-networked last.",[7584,7585,7586,7592,7603,7630,7643],"ol",{},[2165,7587,7588,7591],{},[14,7589,7590],{},"sessionStorage"," cache. The user already resolved to a country during this session; we don't redo the work.",[2165,7593,7594,7595,7598,7599,7602],{},"IP geolocation via ",[14,7596,7597],{},"ipapi.co"," (configurable). The most accurate signal but it costs a network call and a tiny bit of privacy. ",[14,7600,7601],{},"strategy: 'locale'"," skips this step for consumers who don't want the request.",[2165,7604,7605,7606,7609,7610,7613,7614,283,7617,7613,7620,283,7623,7613,7626,7629],{},"Timezone, via ",[14,7607,7608],{},"Intl.DateTimeFormat().resolvedOptions().timeZone",", looked up against a hand-rolled table that maps ",[14,7611,7612],{},"Africa\u002FCairo"," to ",[14,7615,7616],{},"EG",[14,7618,7619],{},"Asia\u002FRiyadh",[14,7621,7622],{},"SA",[14,7624,7625],{},"Europe\u002FLondon",[14,7627,7628],{},"GB",", and so on. The map covers the most-populated zones; it's honest about not covering every one. A miss falls through.",[2165,7631,7632,7635,7636,1389,7639,7642],{},[14,7633,7634],{},"navigator.language",", which gives ",[14,7637,7638],{},"en-EG",[14,7640,7641],{},"ar-SA"," and similar. The region suffix is the country. Less reliable (users move; browser locales don't), but free.",[2165,7644,7645,7646,7649],{},"The caller-supplied default. ",[14,7647,7648],{},"defaultCountry: 'US'"," is the library default; consumers can pick their own.",[10,7651,7652],{},"The IP step is the one that needed real care, because the call can hang:",[40,7654,7656],{"className":186,"code":7655,"language":188,"meta":45,"style":45},"async function tryIp(endpoint: string, timeoutMs: number): Promise\u003Cstring | null> {\n  if (!isBrowser() || typeof fetch !== 'function') return null;\n  const controller = new AbortController();\n  const timer = setTimeout(() => controller.abort(), timeoutMs);\n  try {\n    const res = await fetch(endpoint, { signal: controller.signal, credentials: 'omit' });\n    if (!res.ok) return null;\n    const data = (await res.json()) as { country_code?: string; country?: string };\n    const code = (data.country_code ?? data.country ?? '').toString().toUpperCase();\n    return \u002F^[A-Z]{2}$\u002F.test(code) ? code : null;\n  } catch {\n    return null;\n  } finally {\n    clearTimeout(timer);\n  }\n}\n",[14,7657,7658,7701,7733,7749,7774,7780,7802,7819,7863,7896,7933,7941,7949,7958,7966,7970],{"__ignoreMap":45},[49,7659,7660,7663,7665,7667,7669,7672,7674,7676,7678,7681,7683,7685,7687,7689,7691,7693,7695,7697,7699],{"class":51,"line":52},[49,7661,7662],{"class":195},"async",[49,7664,7264],{"class":195},[49,7666,7473],{"class":202},[49,7668,1072],{"class":206},[49,7670,7671],{"class":225},"endpoint",[49,7673,229],{"class":195},[49,7675,232],{"class":216},[49,7677,283],{"class":206},[49,7679,7680],{"class":225},"timeoutMs",[49,7682,229],{"class":195},[49,7684,444],{"class":216},[49,7686,1642],{"class":206},[49,7688,229],{"class":195},[49,7690,7287],{"class":202},[49,7692,207],{"class":206},[49,7694,280],{"class":216},[49,7696,235],{"class":195},[49,7698,4657],{"class":216},[49,7700,220],{"class":206},[49,7702,7703,7705,7707,7709,7712,7714,7716,7718,7721,7723,7725,7727,7729,7731],{"class":51,"line":58},[49,7704,1269],{"class":195},[49,7706,419],{"class":206},[49,7708,1274],{"class":195},[49,7710,7711],{"class":202},"isBrowser",[49,7713,1569],{"class":206},[49,7715,6626],{"class":195},[49,7717,1290],{"class":195},[49,7719,7720],{"class":206}," fetch ",[49,7722,1948],{"class":195},[49,7724,1299],{"class":310},[49,7726,439],{"class":206},[49,7728,1280],{"class":195},[49,7730,4657],{"class":216},[49,7732,324],{"class":206},[49,7734,7735,7737,7740,7742,7744,7747],{"class":51,"line":64},[49,7736,1438],{"class":195},[49,7738,7739],{"class":216}," controller",[49,7741,213],{"class":195},[49,7743,2278],{"class":195},[49,7745,7746],{"class":202}," AbortController",[49,7748,1998],{"class":206},[49,7750,7751,7753,7756,7758,7761,7763,7765,7768,7771],{"class":51,"line":70},[49,7752,1438],{"class":195},[49,7754,7755],{"class":216}," timer",[49,7757,213],{"class":195},[49,7759,7760],{"class":202}," setTimeout",[49,7762,962],{"class":206},[49,7764,361],{"class":195},[49,7766,7767],{"class":206}," controller.",[49,7769,7770],{"class":202},"abort",[49,7772,7773],{"class":206},"(), timeoutMs);\n",[49,7775,7776,7778],{"class":51,"line":76},[49,7777,6417],{"class":195},[49,7779,967],{"class":206},[49,7781,7782,7784,7787,7789,7791,7794,7797,7800],{"class":51,"line":82},[49,7783,3223],{"class":195},[49,7785,7786],{"class":216}," res",[49,7788,213],{"class":195},[49,7790,7470],{"class":195},[49,7792,7793],{"class":202}," fetch",[49,7795,7796],{"class":206},"(endpoint, { signal: controller.signal, credentials: ",[49,7798,7799],{"class":310},"'omit'",[49,7801,5879],{"class":206},[49,7803,7804,7806,7808,7810,7813,7815,7817],{"class":51,"line":88},[49,7805,1698],{"class":195},[49,7807,419],{"class":206},[49,7809,1274],{"class":195},[49,7811,7812],{"class":206},"res.ok) ",[49,7814,1280],{"class":195},[49,7816,4657],{"class":216},[49,7818,324],{"class":206},[49,7820,7821,7823,7826,7828,7830,7833,7836,7839,7842,7844,7846,7849,7851,7853,7855,7857,7859,7861],{"class":51,"line":94},[49,7822,3223],{"class":195},[49,7824,7825],{"class":216}," data",[49,7827,213],{"class":195},[49,7829,419],{"class":206},[49,7831,7832],{"class":195},"await",[49,7834,7835],{"class":206}," res.",[49,7837,7838],{"class":202},"json",[49,7840,7841],{"class":206},"()) ",[49,7843,1536],{"class":195},[49,7845,2855],{"class":206},[49,7847,7848],{"class":225},"country_code",[49,7850,272],{"class":195},[49,7852,232],{"class":216},[49,7854,2865],{"class":206},[49,7856,5910],{"class":225},[49,7858,272],{"class":195},[49,7860,232],{"class":216},[49,7862,2875],{"class":206},[49,7864,7865,7867,7870,7872,7875,7877,7880,7882,7884,7886,7889,7892,7894],{"class":51,"line":100},[49,7866,3223],{"class":195},[49,7868,7869],{"class":216}," code",[49,7871,213],{"class":195},[49,7873,7874],{"class":206}," (data.country_code ",[49,7876,6493],{"class":195},[49,7878,7879],{"class":206}," data.country ",[49,7881,6493],{"class":195},[49,7883,6111],{"class":310},[49,7885,1669],{"class":206},[49,7887,7888],{"class":202},"toString",[49,7890,7891],{"class":206},"().",[49,7893,7436],{"class":202},[49,7895,1998],{"class":206},[49,7897,7898,7900,7902,7905,7908,7911,7914,7916,7919,7922,7924,7927,7929,7931],{"class":51,"line":106},[49,7899,1557],{"class":195},[49,7901,5087],{"class":310},[49,7903,7904],{"class":195},"^",[49,7906,7907],{"class":216},"[A-Z]",[49,7909,7910],{"class":195},"{2}$",[49,7912,7913],{"class":310},"\u002F",[49,7915,180],{"class":206},[49,7917,7918],{"class":202},"test",[49,7920,7921],{"class":206},"(code) ",[49,7923,986],{"class":195},[49,7925,7926],{"class":206}," code ",[49,7928,229],{"class":195},[49,7930,4657],{"class":216},[49,7932,324],{"class":206},[49,7934,7935,7937,7939],{"class":51,"line":112},[49,7936,2942],{"class":206},[49,7938,6514],{"class":195},[49,7940,967],{"class":206},[49,7942,7943,7945,7947],{"class":51,"line":119},[49,7944,1557],{"class":195},[49,7946,4657],{"class":216},[49,7948,324],{"class":206},[49,7950,7951,7953,7956],{"class":51,"line":125},[49,7952,2942],{"class":206},[49,7954,7955],{"class":195},"finally",[49,7957,967],{"class":206},[49,7959,7960,7963],{"class":51,"line":131},[49,7961,7962],{"class":202},"    clearTimeout",[49,7964,7965],{"class":206},"(timer);\n",[49,7967,7968],{"class":51,"line":137},[49,7969,109],{"class":206},[49,7971,7972],{"class":51,"line":143},[49,7973,169],{"class":206},[10,7975,7976,7979,7980,512,7982,7985,7986,7989,7990,7993,7994,7993,7997,7999],{},[14,7977,7978],{},"AbortController"," plus a 2-second timer means a slow IP service can't block the detection chain for longer than ",[14,7981,7680],{},[14,7983,7984],{},"credentials: 'omit'"," keeps cookies out of the request to whatever third party you've configured. The shape of the response is checked with ",[14,7987,7988],{},"\u002F^[A-Z]{2}$\u002F"," because some IP APIs return ",[14,7991,7992],{},"\"GB\"",", some return ",[14,7995,7996],{},"\"United Kingdom\"",[14,7998,4198],{}," when they don't know. The regex is the gate.",[10,8001,8002,8005],{},[14,8003,8004],{},"detectCountry"," lives outside the component on purpose:",[40,8007,8009],{"className":186,"code":8008,"language":188,"meta":45,"style":45},"import { detectCountry } from '@alikhalilll\u002Fui\u002Ftell-input';\n\nconst country = await detectCountry({ strategy: 'locale' });\n",[14,8010,8011,8025,8029],{"__ignoreMap":45},[49,8012,8013,8015,8018,8020,8023],{"class":51,"line":52},[49,8014,4324],{"class":195},[49,8016,8017],{"class":206}," { detectCountry } ",[49,8019,4335],{"class":195},[49,8021,8022],{"class":310}," '@alikhalilll\u002Fui\u002Ftell-input'",[49,8024,324],{"class":206},[49,8026,8027],{"class":51,"line":58},[49,8028,116],{"emptyLinePlaceholder":115},[49,8030,8031,8033,8035,8037,8039,8041,8044,8047],{"class":51,"line":64},[49,8032,951],{"class":195},[49,8034,5980],{"class":216},[49,8036,213],{"class":195},[49,8038,7470],{"class":195},[49,8040,7267],{"class":202},[49,8042,8043],{"class":206},"({ strategy: ",[49,8045,8046],{"class":310},"'locale'",[49,8048,5879],{"class":206},[10,8050,8051,8052,8055,8056,8059],{},"No Vue dependency, no reactive refs, no ",[14,8053,8054],{},"onMounted",". It works in a server middleware that wants to set a cookie, in a CLI that wants to print where the user probably is, in a unit test that wants to control the strategy. The reactive wrapper (",[14,8057,8058],{},"useCountryDetection",") is a thin shell around it for the in-component case. Two surfaces, one implementation.",[10,8061,8062],{},"And the whole chain is pluggable. If you already detect geo server-side and pass it down through SSR, you don't want the component re-running an IP fetch:",[40,8064,8066],{"className":587,"code":8065,"language":589,"meta":45,"style":45},"\u003CATellInput\n  v-model:phone=\"phone\"\n  v-model:country=\"country\"\n  :detector=\"(opts) => Promise.resolve(serverDetectedCountry.value)\"\n\u002F>\n",[14,8067,8068,8075,8093,8109,8141],{"__ignoreMap":45},[49,8069,8070,8072],{"class":51,"line":52},[49,8071,207],{"class":206},[49,8073,8074],{"class":598},"ATellInput\n",[49,8076,8077,8080,8082,8085,8087,8089,8091],{"class":51,"line":58},[49,8078,8079],{"class":202},"  v-model",[49,8081,229],{"class":206},[49,8083,8084],{"class":202},"phone",[49,8086,638],{"class":206},[49,8088,3703],{"class":310},[49,8090,8084],{"class":206},[49,8092,3709],{"class":310},[49,8094,8095,8097,8099,8101,8103,8105,8107],{"class":51,"line":64},[49,8096,8079],{"class":202},[49,8098,229],{"class":206},[49,8100,5910],{"class":202},[49,8102,638],{"class":206},[49,8104,3703],{"class":310},[49,8106,5910],{"class":206},[49,8108,3709],{"class":310},[49,8110,8111,8114,8117,8119,8121,8123,8125,8127,8129,8131,8133,8136,8139],{"class":51,"line":70},[49,8112,8113],{"class":206},"  :",[49,8115,8116],{"class":202},"detector",[49,8118,638],{"class":206},[49,8120,3703],{"class":310},[49,8122,1072],{"class":206},[49,8124,7272],{"class":225},[49,8126,439],{"class":206},[49,8128,361],{"class":195},[49,8130,7287],{"class":216},[49,8132,180],{"class":206},[49,8134,8135],{"class":202},"resolve",[49,8137,8138],{"class":206},"(serverDetectedCountry.value)",[49,8140,3709],{"class":310},[49,8142,8143],{"class":51,"line":76},[49,8144,8145],{"class":206},"\u002F>\n",[10,8147,891,8148,8150,8151,8154],{},[14,8149,8116],{}," prop replaces the built-in chain entirely. The component still runs the rest of the detection logic (sets the inferred country, uses it as a Tier 2 hint, etc.), but the ",[503,8152,8153],{},"source"," of the answer becomes whatever you said it was.",[29,8156,8158],{"id":8157},"responsive-picker-one-component-two-presentations","Responsive picker: one component, two presentations",[10,8160,8161],{},"The old country dropdown was a popover at every viewport size. On a phone, that meant a 250-row scrollable list anchored to a 38-pixel-wide trigger button, with a search input that the on-screen keyboard kept occluding. It worked. It worked badly.",[10,8163,8164,8165,8168],{},"The new picker uses the ",[14,8166,8167],{},"AResponsivePopover"," primitive from the same library, which is a popover on desktop and a vaul-vue drawer on mobile:",[40,8170,8172],{"className":587,"code":8171,"language":589,"meta":45,"style":45},"\u003CAResponsivePopover v-model:open=\"open\">\n  \u003CAResponsivePopoverTrigger as-child>\n    \u003C!-- trigger button -->\n  \u003C\u002FAResponsivePopoverTrigger>\n\n  \u003CAResponsivePopoverContent\n    :popover-class=\"\n      cn(\n        'w-[min(20rem,calc(100vw-2rem))] max-h-[min(22rem,var(--reka-popover-content-available-height))]',\n        props.popoverClass\n      )\n    \"\n    :drawer-class=\"cn('max-h-[80vh] pb-4', props.drawerClass)\"\n  >\n    \u003C!-- search + list -->\n  \u003C\u002FAResponsivePopoverContent>\n\u003C\u002FAResponsivePopover>\n",[14,8173,8174,8198,8203,8208,8213,8217,8222,8227,8232,8237,8242,8247,8252,8257,8262,8267,8272],{"__ignoreMap":45},[49,8175,8176,8178,8180,8183,8185,8188,8190,8192,8194,8196],{"class":51,"line":52},[49,8177,207],{"class":206},[49,8179,8167],{"class":598},[49,8181,8182],{"class":202}," v-model",[49,8184,229],{"class":206},[49,8186,8187],{"class":202},"open",[49,8189,638],{"class":206},[49,8191,3703],{"class":310},[49,8193,8187],{"class":206},[49,8195,3703],{"class":310},[49,8197,602],{"class":206},[49,8199,8200],{"class":51,"line":58},[49,8201,8202],{"class":206},"  \u003CAResponsivePopoverTrigger as-child>\n",[49,8204,8205],{"class":51,"line":64},[49,8206,8207],{"class":206},"    \u003C!-- trigger button -->\n",[49,8209,8210],{"class":51,"line":70},[49,8211,8212],{"class":206},"  \u003C\u002FAResponsivePopoverTrigger>\n",[49,8214,8215],{"class":51,"line":76},[49,8216,116],{"emptyLinePlaceholder":115},[49,8218,8219],{"class":51,"line":82},[49,8220,8221],{"class":206},"  \u003CAResponsivePopoverContent\n",[49,8223,8224],{"class":51,"line":88},[49,8225,8226],{"class":206},"    :popover-class=\"\n",[49,8228,8229],{"class":51,"line":94},[49,8230,8231],{"class":206},"      cn(\n",[49,8233,8234],{"class":51,"line":100},[49,8235,8236],{"class":206},"        'w-[min(20rem,calc(100vw-2rem))] max-h-[min(22rem,var(--reka-popover-content-available-height))]',\n",[49,8238,8239],{"class":51,"line":106},[49,8240,8241],{"class":206},"        props.popoverClass\n",[49,8243,8244],{"class":51,"line":112},[49,8245,8246],{"class":206},"      )\n",[49,8248,8249],{"class":51,"line":119},[49,8250,8251],{"class":206},"    \"\n",[49,8253,8254],{"class":51,"line":125},[49,8255,8256],{"class":206},"    :drawer-class=\"cn('max-h-[80vh] pb-4', props.drawerClass)\"\n",[49,8258,8259],{"class":51,"line":131},[49,8260,8261],{"class":206},"  >\n",[49,8263,8264],{"class":51,"line":137},[49,8265,8266],{"class":206},"    \u003C!-- search + list -->\n",[49,8268,8269],{"class":51,"line":143},[49,8270,8271],{"class":206},"  \u003C\u002FAResponsivePopoverContent>\n",[49,8273,8274,8276,8278],{"class":51,"line":149},[49,8275,738],{"class":206},[49,8277,8167],{"class":598},[49,8279,602],{"class":206},[10,8281,8282,8283,5264,8286,8289,8290,8293,8294,8297],{},"Two class props (",[14,8284,8285],{},"popoverClass",[14,8287,8288],{},"drawerClass",") get forwarded separately, so consumers can theme each presentation without the other inheriting it. The popover wants ",[14,8291,8292],{},"max-h-[min(22rem,var(--reka-popover-content-available-height))]"," because reka-ui exposes the available height as a CSS variable; the drawer wants ",[14,8295,8296],{},"max-h-[80vh]"," because it's a bottom-sheet and \"80% of the viewport\" is a sensible cap. Forcing one set of classes onto both would be wrong for one of them; allowing the consumer to override either separately is exactly the seam a library needs.",[10,8299,8300],{},"The other side benefit is search. In drawer mode, the search input docks at the top of a sheet that takes up most of the screen, and the keyboard pushes it up rather than burying it. The same code, the same component, the right shape for the device. Not a media-query inside the consumer's CSS; a primitive that knows the difference.",[29,8302,8304],{"id":8303},"slots-for-everything-fourteen-of-them","Slots for everything (fourteen of them)",[10,8306,8307],{},"The old version was monolithic. Restyling the error message meant patching the component. Changing the chevron icon meant patching the component. Replacing the country flag with one from your own CDN meant patching the component.",[10,8309,8310],{},"The new version owns the behaviour and lets the consumer own the look:",[40,8312,8314],{"className":186,"code":8313,"language":188,"meta":45,"style":45},"defineSlots\u003C{\n  prefix?: () => unknown;\n  suffix?: (props: { validationState: 'idle' | 'valid' | 'error'; validation: PhoneValidationResult }) => unknown;\n  'valid-icon'?: () => unknown;\n  'error-icon'?: (props: { reason: string }) => unknown;\n  hint?: (props: { country: string; formatHint: string; example: string | null }) => unknown;\n  error?: (props: { message: string; reason: string; validation: PhoneValidationResult }) => unknown;\n  trigger?: (props: { selectedCountry: CountryOption | null; open: boolean; sizeClasses: string }) => unknown;\n  chevron?: (props: { open: boolean }) => unknown;\n  flag?: (props: { country: CountryOption; context: 'trigger' | 'item' }) => unknown;\n  item?: (props: { country: CountryOption; selected: boolean; disabled: boolean; select: () => void }) => unknown;\n  'group-header'?: (props: { label: string; group: 'suggested' | 'all' }) => unknown;\n  search?: (props: { value: string; setValue: (v: string) => void; isSearching: boolean }) => unknown;\n  loading?: () => unknown;\n  empty?: (props: { query: string }) => unknown;\n}>();\n",[14,8315,8316,8324,8339,8391,8406,8436,8487,8533,8585,8614,8658,8719,8764,8825,8840,8870],{"__ignoreMap":45},[49,8317,8318,8321],{"class":51,"line":52},[49,8319,8320],{"class":202},"defineSlots",[49,8322,8323],{"class":206},"\u003C{\n",[49,8325,8326,8329,8331,8333,8335,8337],{"class":51,"line":58},[49,8327,8328],{"class":202},"  prefix",[49,8330,272],{"class":195},[49,8332,358],{"class":206},[49,8334,361],{"class":195},[49,8336,217],{"class":216},[49,8338,324],{"class":206},[49,8340,8341,8344,8346,8348,8350,8352,8354,8357,8359,8362,8364,8367,8369,8372,8374,8377,8379,8382,8385,8387,8389],{"class":51,"line":64},[49,8342,8343],{"class":202},"  suffix",[49,8345,272],{"class":195},[49,8347,419],{"class":206},[49,8349,4726],{"class":225},[49,8351,229],{"class":195},[49,8353,2855],{"class":206},[49,8355,8356],{"class":225},"validationState",[49,8358,229],{"class":195},[49,8360,8361],{"class":310}," 'idle'",[49,8363,235],{"class":195},[49,8365,8366],{"class":310}," 'valid'",[49,8368,235],{"class":195},[49,8370,8371],{"class":310}," 'error'",[49,8373,2865],{"class":206},[49,8375,8376],{"class":225},"validation",[49,8378,229],{"class":195},[49,8380,8381],{"class":202}," PhoneValidationResult",[49,8383,8384],{"class":206}," }) ",[49,8386,361],{"class":195},[49,8388,217],{"class":216},[49,8390,324],{"class":206},[49,8392,8393,8396,8398,8400,8402,8404],{"class":51,"line":70},[49,8394,8395],{"class":310},"  'valid-icon'",[49,8397,272],{"class":195},[49,8399,358],{"class":206},[49,8401,361],{"class":195},[49,8403,217],{"class":216},[49,8405,324],{"class":206},[49,8407,8408,8411,8413,8415,8417,8419,8421,8424,8426,8428,8430,8432,8434],{"class":51,"line":76},[49,8409,8410],{"class":310},"  'error-icon'",[49,8412,272],{"class":195},[49,8414,419],{"class":206},[49,8416,4726],{"class":225},[49,8418,229],{"class":195},[49,8420,2855],{"class":206},[49,8422,8423],{"class":225},"reason",[49,8425,229],{"class":195},[49,8427,232],{"class":216},[49,8429,8384],{"class":206},[49,8431,361],{"class":195},[49,8433,217],{"class":216},[49,8435,324],{"class":206},[49,8437,8438,8441,8443,8445,8447,8449,8451,8453,8455,8457,8459,8462,8464,8466,8468,8471,8473,8475,8477,8479,8481,8483,8485],{"class":51,"line":82},[49,8439,8440],{"class":202},"  hint",[49,8442,272],{"class":195},[49,8444,419],{"class":206},[49,8446,4726],{"class":225},[49,8448,229],{"class":195},[49,8450,2855],{"class":206},[49,8452,5910],{"class":225},[49,8454,229],{"class":195},[49,8456,232],{"class":216},[49,8458,2865],{"class":206},[49,8460,8461],{"class":225},"formatHint",[49,8463,229],{"class":195},[49,8465,232],{"class":216},[49,8467,2865],{"class":206},[49,8469,8470],{"class":225},"example",[49,8472,229],{"class":195},[49,8474,232],{"class":216},[49,8476,235],{"class":195},[49,8478,4657],{"class":216},[49,8480,8384],{"class":206},[49,8482,361],{"class":195},[49,8484,217],{"class":216},[49,8486,324],{"class":206},[49,8488,8489,8492,8494,8496,8498,8500,8502,8505,8507,8509,8511,8513,8515,8517,8519,8521,8523,8525,8527,8529,8531],{"class":51,"line":88},[49,8490,8491],{"class":202},"  error",[49,8493,272],{"class":195},[49,8495,419],{"class":206},[49,8497,4726],{"class":225},[49,8499,229],{"class":195},[49,8501,2855],{"class":206},[49,8503,8504],{"class":225},"message",[49,8506,229],{"class":195},[49,8508,232],{"class":216},[49,8510,2865],{"class":206},[49,8512,8423],{"class":225},[49,8514,229],{"class":195},[49,8516,232],{"class":216},[49,8518,2865],{"class":206},[49,8520,8376],{"class":225},[49,8522,229],{"class":195},[49,8524,8381],{"class":202},[49,8526,8384],{"class":206},[49,8528,361],{"class":195},[49,8530,217],{"class":216},[49,8532,324],{"class":206},[49,8534,8535,8538,8540,8542,8544,8546,8548,8551,8553,8556,8558,8560,8562,8564,8566,8568,8570,8573,8575,8577,8579,8581,8583],{"class":51,"line":94},[49,8536,8537],{"class":202},"  trigger",[49,8539,272],{"class":195},[49,8541,419],{"class":206},[49,8543,4726],{"class":225},[49,8545,229],{"class":195},[49,8547,2855],{"class":206},[49,8549,8550],{"class":225},"selectedCountry",[49,8552,229],{"class":195},[49,8554,8555],{"class":202}," CountryOption",[49,8557,235],{"class":195},[49,8559,4657],{"class":216},[49,8561,2865],{"class":206},[49,8563,8187],{"class":225},[49,8565,229],{"class":195},[49,8567,334],{"class":216},[49,8569,2865],{"class":206},[49,8571,8572],{"class":225},"sizeClasses",[49,8574,229],{"class":195},[49,8576,232],{"class":216},[49,8578,8384],{"class":206},[49,8580,361],{"class":195},[49,8582,217],{"class":216},[49,8584,324],{"class":206},[49,8586,8587,8590,8592,8594,8596,8598,8600,8602,8604,8606,8608,8610,8612],{"class":51,"line":100},[49,8588,8589],{"class":202},"  chevron",[49,8591,272],{"class":195},[49,8593,419],{"class":206},[49,8595,4726],{"class":225},[49,8597,229],{"class":195},[49,8599,2855],{"class":206},[49,8601,8187],{"class":225},[49,8603,229],{"class":195},[49,8605,334],{"class":216},[49,8607,8384],{"class":206},[49,8609,361],{"class":195},[49,8611,217],{"class":216},[49,8613,324],{"class":206},[49,8615,8616,8619,8621,8623,8625,8627,8629,8631,8633,8635,8637,8640,8642,8645,8647,8650,8652,8654,8656],{"class":51,"line":106},[49,8617,8618],{"class":202},"  flag",[49,8620,272],{"class":195},[49,8622,419],{"class":206},[49,8624,4726],{"class":225},[49,8626,229],{"class":195},[49,8628,2855],{"class":206},[49,8630,5910],{"class":225},[49,8632,229],{"class":195},[49,8634,8555],{"class":202},[49,8636,2865],{"class":206},[49,8638,8639],{"class":225},"context",[49,8641,229],{"class":195},[49,8643,8644],{"class":310}," 'trigger'",[49,8646,235],{"class":195},[49,8648,8649],{"class":310}," 'item'",[49,8651,8384],{"class":206},[49,8653,361],{"class":195},[49,8655,217],{"class":216},[49,8657,324],{"class":206},[49,8659,8660,8663,8665,8667,8669,8671,8673,8675,8677,8679,8681,8684,8686,8688,8690,8693,8695,8697,8699,8702,8704,8706,8708,8711,8713,8715,8717],{"class":51,"line":112},[49,8661,8662],{"class":202},"  item",[49,8664,272],{"class":195},[49,8666,419],{"class":206},[49,8668,4726],{"class":225},[49,8670,229],{"class":195},[49,8672,2855],{"class":206},[49,8674,5910],{"class":225},[49,8676,229],{"class":195},[49,8678,8555],{"class":202},[49,8680,2865],{"class":206},[49,8682,8683],{"class":225},"selected",[49,8685,229],{"class":195},[49,8687,334],{"class":216},[49,8689,2865],{"class":206},[49,8691,8692],{"class":225},"disabled",[49,8694,229],{"class":195},[49,8696,334],{"class":216},[49,8698,2865],{"class":206},[49,8700,8701],{"class":202},"select",[49,8703,229],{"class":195},[49,8705,358],{"class":206},[49,8707,361],{"class":195},[49,8709,8710],{"class":216}," void",[49,8712,8384],{"class":206},[49,8714,361],{"class":195},[49,8716,217],{"class":216},[49,8718,324],{"class":206},[49,8720,8721,8724,8726,8728,8730,8732,8734,8737,8739,8741,8743,8746,8748,8751,8753,8756,8758,8760,8762],{"class":51,"line":119},[49,8722,8723],{"class":310},"  'group-header'",[49,8725,272],{"class":195},[49,8727,419],{"class":206},[49,8729,4726],{"class":225},[49,8731,229],{"class":195},[49,8733,2855],{"class":206},[49,8735,8736],{"class":225},"label",[49,8738,229],{"class":195},[49,8740,232],{"class":216},[49,8742,2865],{"class":206},[49,8744,8745],{"class":225},"group",[49,8747,229],{"class":195},[49,8749,8750],{"class":310}," 'suggested'",[49,8752,235],{"class":195},[49,8754,8755],{"class":310}," 'all'",[49,8757,8384],{"class":206},[49,8759,361],{"class":195},[49,8761,217],{"class":216},[49,8763,324],{"class":206},[49,8765,8766,8769,8771,8773,8775,8777,8779,8782,8784,8786,8788,8791,8793,8795,8798,8800,8802,8804,8806,8808,8810,8813,8815,8817,8819,8821,8823],{"class":51,"line":125},[49,8767,8768],{"class":202},"  search",[49,8770,272],{"class":195},[49,8772,419],{"class":206},[49,8774,4726],{"class":225},[49,8776,229],{"class":195},[49,8778,2855],{"class":206},[49,8780,8781],{"class":225},"value",[49,8783,229],{"class":195},[49,8785,232],{"class":216},[49,8787,2865],{"class":206},[49,8789,8790],{"class":202},"setValue",[49,8792,229],{"class":195},[49,8794,419],{"class":206},[49,8796,8797],{"class":225},"v",[49,8799,229],{"class":195},[49,8801,232],{"class":216},[49,8803,439],{"class":206},[49,8805,361],{"class":195},[49,8807,8710],{"class":216},[49,8809,2865],{"class":206},[49,8811,8812],{"class":225},"isSearching",[49,8814,229],{"class":195},[49,8816,334],{"class":216},[49,8818,8384],{"class":206},[49,8820,361],{"class":195},[49,8822,217],{"class":216},[49,8824,324],{"class":206},[49,8826,8827,8830,8832,8834,8836,8838],{"class":51,"line":131},[49,8828,8829],{"class":202},"  loading",[49,8831,272],{"class":195},[49,8833,358],{"class":206},[49,8835,361],{"class":195},[49,8837,217],{"class":216},[49,8839,324],{"class":206},[49,8841,8842,8845,8847,8849,8851,8853,8855,8858,8860,8862,8864,8866,8868],{"class":51,"line":137},[49,8843,8844],{"class":202},"  empty",[49,8846,272],{"class":195},[49,8848,419],{"class":206},[49,8850,4726],{"class":225},[49,8852,229],{"class":195},[49,8854,2855],{"class":206},[49,8856,8857],{"class":225},"query",[49,8859,229],{"class":195},[49,8861,232],{"class":216},[49,8863,8384],{"class":206},[49,8865,361],{"class":195},[49,8867,217],{"class":216},[49,8869,324],{"class":206},[49,8871,8872],{"class":51,"line":143},[49,8873,8874],{"class":206},"}>();\n",[10,8876,8877,8878,283,8881,8884,8885,283,8888,8891,8892,283,8895,283,8898,8901,8902,283,8905,283,8907,283,8910,283,8913,8915],{},"Fourteen slots, three groups. The visual ones (",[14,8879,8880],{},"prefix",[14,8882,8883],{},"suffix",", the two icon slots, ",[14,8886,8887],{},"hint",[14,8889,8890],{},"error",") decorate the field itself. The picker-trigger ones (",[14,8893,8894],{},"trigger",[14,8896,8897],{},"chevron",[14,8899,8900],{},"flag",") replace the button users click. The picker-content ones (",[14,8903,8904],{},"search",[14,8906,4286],{},[14,8908,8909],{},"empty",[14,8911,8912],{},"group-header",[14,8914,2378],{},") replace anything inside the popover or drawer.",[10,8917,8918,8919,8922],{},"Almost all of the picker slots forward straight to ",[14,8920,8921],{},"ACountrySelect",". The component itself does the routing:",[40,8924,8926],{"className":587,"code":8925,"language":589,"meta":45,"style":45},"\u003Ctemplate v-if=\"$slots.trigger\" #trigger=\"slotProps\">\n  \u003Cslot name=\"trigger\" v-bind=\"slotProps\" \u002F>\n\u003C\u002Ftemplate>\n\u003Ctemplate v-if=\"$slots.chevron\" #chevron=\"slotProps\">\n  \u003Cslot name=\"chevron\" v-bind=\"slotProps\" \u002F>\n\u003C\u002Ftemplate>\n\u003C!-- ... and so on for each forwarded slot -->\n",[14,8927,8928,8961,8988,8996,9027,9050,9058],{"__ignoreMap":45},[49,8929,8930,8932,8934,8936,8938,8940,8943,8945,8948,8950,8952,8954,8957,8959],{"class":51,"line":52},[49,8931,207],{"class":206},[49,8933,599],{"class":598},[49,8935,3851],{"class":195},[49,8937,638],{"class":206},[49,8939,3703],{"class":310},[49,8941,8942],{"class":206},"$slots.trigger",[49,8944,3703],{"class":310},[49,8946,8947],{"class":206}," #",[49,8949,8894],{"class":202},[49,8951,638],{"class":206},[49,8953,3703],{"class":310},[49,8955,8956],{"class":206},"slotProps",[49,8958,3703],{"class":310},[49,8960,602],{"class":206},[49,8962,8963,8965,8968,8971,8973,8976,8979,8981,8984,8986],{"class":51,"line":58},[49,8964,607],{"class":206},[49,8966,8967],{"class":598},"slot",[49,8969,8970],{"class":202}," name",[49,8972,638],{"class":206},[49,8974,8975],{"class":310},"\"trigger\"",[49,8977,8978],{"class":202}," v-bind",[49,8980,638],{"class":206},[49,8982,8983],{"class":310},"\"slotProps\"",[49,8985,5087],{"class":5086},[49,8987,602],{"class":206},[49,8989,8990,8992,8994],{"class":51,"line":64},[49,8991,738],{"class":206},[49,8993,599],{"class":598},[49,8995,602],{"class":206},[49,8997,8998,9000,9002,9004,9006,9008,9011,9013,9015,9017,9019,9021,9023,9025],{"class":51,"line":70},[49,8999,207],{"class":206},[49,9001,599],{"class":598},[49,9003,3851],{"class":195},[49,9005,638],{"class":206},[49,9007,3703],{"class":310},[49,9009,9010],{"class":206},"$slots.chevron",[49,9012,3703],{"class":310},[49,9014,8947],{"class":206},[49,9016,8897],{"class":202},[49,9018,638],{"class":206},[49,9020,3703],{"class":310},[49,9022,8956],{"class":206},[49,9024,3703],{"class":310},[49,9026,602],{"class":206},[49,9028,9029,9031,9033,9035,9037,9040,9042,9044,9046,9048],{"class":51,"line":76},[49,9030,607],{"class":206},[49,9032,8967],{"class":598},[49,9034,8970],{"class":202},[49,9036,638],{"class":206},[49,9038,9039],{"class":310},"\"chevron\"",[49,9041,8978],{"class":202},[49,9043,638],{"class":206},[49,9045,8983],{"class":310},[49,9047,5087],{"class":5086},[49,9049,602],{"class":206},[49,9051,9052,9054,9056],{"class":51,"line":82},[49,9053,738],{"class":206},[49,9055,599],{"class":598},[49,9057,602],{"class":206},[49,9059,9060],{"class":51,"line":88},[49,9061,9062],{"class":1852},"\u003C!-- ... and so on for each forwarded slot -->\n",[10,9064,891,9065,9068,9069,9072,9073,9075],{},[14,9066,9067],{},"v-if=\"$slots.x\""," matters. Without it, a forwarded ",[14,9070,9071],{},"\u003Ctemplate #x>"," always exists from the parent's perspective, and ",[14,9074,8921],{},"'s own fallback rendering never gets a chance to run. The guard says \"only forward if the consumer actually provided this slot.\"",[10,9077,9078],{},"A realistic customization:",[40,9080,9082],{"className":587,"code":9081,"language":589,"meta":45,"style":45},"\u003CATellInput v-model:phone=\"phone\" v-model:country=\"country\" show-validation>\n  \u003Ctemplate #suffix=\"{ validationState }\">\n    \u003CSparkle v-if=\"validationState === 'valid'\" class=\"size-4 text-amber-400\" \u002F>\n  \u003C\u002Ftemplate>\n  \u003Ctemplate #error=\"{ message, reason }\">\n    \u003Cp class=\"text-destructive text-xs\">\n      \u003Ccode class=\"bg-muted rounded px-1 py-0.5\">{{ reason }}\u003C\u002Fcode> {{ message }}\n    \u003C\u002Fp>\n  \u003C\u002Ftemplate>\n  \u003Ctemplate #empty=\"{ query }\">\n    \u003Cdiv class=\"px-3 py-6 text-center text-sm\">\n      Nothing matched \"\u003Cstrong>{{ query }}\u003C\u002Fstrong>\".\n      \u003Cbutton type=\"button\" class=\"underline\" @click=\"suggestCountry(query)\">\n        Suggest a country\n      \u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  \u003C\u002Ftemplate>\n\u003C\u002FATellInput>\n",[14,9083,9084,9123,9128,9133,9138,9143,9148,9153,9158,9162,9167,9172,9177,9182,9187,9192,9197,9201],{"__ignoreMap":45},[49,9085,9086,9088,9090,9092,9094,9096,9098,9100,9102,9104,9106,9108,9110,9112,9114,9116,9118,9121],{"class":51,"line":52},[49,9087,207],{"class":206},[49,9089,5824],{"class":598},[49,9091,8182],{"class":202},[49,9093,229],{"class":206},[49,9095,8084],{"class":202},[49,9097,638],{"class":206},[49,9099,3703],{"class":310},[49,9101,8084],{"class":206},[49,9103,3703],{"class":310},[49,9105,8182],{"class":202},[49,9107,229],{"class":206},[49,9109,5910],{"class":202},[49,9111,638],{"class":206},[49,9113,3703],{"class":310},[49,9115,5910],{"class":206},[49,9117,3703],{"class":310},[49,9119,9120],{"class":202}," show-validation",[49,9122,602],{"class":206},[49,9124,9125],{"class":51,"line":58},[49,9126,9127],{"class":206},"  \u003Ctemplate #suffix=\"{ validationState }\">\n",[49,9129,9130],{"class":51,"line":64},[49,9131,9132],{"class":206},"    \u003CSparkle v-if=\"validationState === 'valid'\" class=\"size-4 text-amber-400\" \u002F>\n",[49,9134,9135],{"class":51,"line":70},[49,9136,9137],{"class":206},"  \u003C\u002Ftemplate>\n",[49,9139,9140],{"class":51,"line":76},[49,9141,9142],{"class":206},"  \u003Ctemplate #error=\"{ message, reason }\">\n",[49,9144,9145],{"class":51,"line":82},[49,9146,9147],{"class":206},"    \u003Cp class=\"text-destructive text-xs\">\n",[49,9149,9150],{"class":51,"line":88},[49,9151,9152],{"class":206},"      \u003Ccode class=\"bg-muted rounded px-1 py-0.5\">{{ reason }}\u003C\u002Fcode> {{ message }}\n",[49,9154,9155],{"class":51,"line":94},[49,9156,9157],{"class":206},"    \u003C\u002Fp>\n",[49,9159,9160],{"class":51,"line":100},[49,9161,9137],{"class":206},[49,9163,9164],{"class":51,"line":106},[49,9165,9166],{"class":206},"  \u003Ctemplate #empty=\"{ query }\">\n",[49,9168,9169],{"class":51,"line":112},[49,9170,9171],{"class":206},"    \u003Cdiv class=\"px-3 py-6 text-center text-sm\">\n",[49,9173,9174],{"class":51,"line":119},[49,9175,9176],{"class":206},"      Nothing matched \"\u003Cstrong>{{ query }}\u003C\u002Fstrong>\".\n",[49,9178,9179],{"class":51,"line":125},[49,9180,9181],{"class":206},"      \u003Cbutton type=\"button\" class=\"underline\" @click=\"suggestCountry(query)\">\n",[49,9183,9184],{"class":51,"line":131},[49,9185,9186],{"class":206},"        Suggest a country\n",[49,9188,9189],{"class":51,"line":137},[49,9190,9191],{"class":206},"      \u003C\u002Fbutton>\n",[49,9193,9194],{"class":51,"line":143},[49,9195,9196],{"class":206},"    \u003C\u002Fdiv>\n",[49,9198,9199],{"class":51,"line":149},[49,9200,9137],{"class":206},[49,9202,9203,9205,9207],{"class":51,"line":155},[49,9204,738],{"class":206},[49,9206,5824],{"class":598},[49,9208,602],{"class":206},[10,9210,9211,9212,9215],{},"A library component's job is to own ",[503,9213,9214],{},"behaviour"," (the parsing, the validation, the detection, the state) and to leave the consumer in charge of what those things look like. Fourteen slots is what that bargain costs, and it's worth it.",[10,9217,9218],{},"The same principle applies to the props that aren't visual:",[40,9220,9222],{"className":186,"code":9221,"language":188,"meta":45,"style":45},"flagUrl?: (iso2: string, width: number) => string;\nsearcher?: (query: string, country: CountryOption) => boolean;\ncountries?: CountryOption[];\ndetector?: (options: DetectCountryOptions) => Promise\u003Cstring | null | undefined>;\nerrorMessages?: Record\u003CPhoneValidationReason, string>;\n",[14,9223,9224,9256,9286,9296,9335],{"__ignoreMap":45},[49,9225,9226,9229,9231,9233,9236,9238,9240,9242,9245,9247,9249,9251,9253],{"class":51,"line":52},[49,9227,9228],{"class":206},"flagUrl",[49,9230,272],{"class":195},[49,9232,419],{"class":206},[49,9234,9235],{"class":225},"iso2",[49,9237,229],{"class":195},[49,9239,232],{"class":216},[49,9241,283],{"class":206},[49,9243,9244],{"class":225},"width",[49,9246,229],{"class":195},[49,9248,444],{"class":216},[49,9250,439],{"class":206},[49,9252,361],{"class":195},[49,9254,9255],{"class":206}," string;\n",[49,9257,9258,9261,9263,9265,9267,9269,9271,9273,9275,9277,9279,9281,9283],{"class":51,"line":58},[49,9259,9260],{"class":206},"searcher",[49,9262,272],{"class":195},[49,9264,419],{"class":206},[49,9266,8857],{"class":225},[49,9268,229],{"class":195},[49,9270,232],{"class":216},[49,9272,283],{"class":206},[49,9274,5910],{"class":225},[49,9276,229],{"class":195},[49,9278,8555],{"class":202},[49,9280,439],{"class":206},[49,9282,361],{"class":195},[49,9284,9285],{"class":206}," boolean;\n",[49,9287,9288,9291,9293],{"class":51,"line":64},[49,9289,9290],{"class":206},"countries",[49,9292,272],{"class":195},[49,9294,9295],{"class":206}," CountryOption[];\n",[49,9297,9298,9300,9302,9304,9307,9309,9311,9313,9315,9317,9319,9322,9324,9326,9328,9330,9333],{"class":51,"line":70},[49,9299,8116],{"class":206},[49,9301,272],{"class":195},[49,9303,419],{"class":206},[49,9305,9306],{"class":225},"options",[49,9308,229],{"class":195},[49,9310,7277],{"class":202},[49,9312,439],{"class":206},[49,9314,361],{"class":195},[49,9316,7287],{"class":216},[49,9318,207],{"class":195},[49,9320,9321],{"class":206},"string ",[49,9323,292],{"class":195},[49,9325,4657],{"class":216},[49,9327,235],{"class":195},[49,9329,1751],{"class":216},[49,9331,9332],{"class":195},">",[49,9334,324],{"class":206},[49,9336,9337,9340,9342,9344,9346,9349,9351],{"class":51,"line":76},[49,9338,9339],{"class":206},"errorMessages",[49,9341,272],{"class":195},[49,9343,275],{"class":206},[49,9345,207],{"class":195},[49,9347,9348],{"class":206},"PhoneValidationReason, string",[49,9350,9332],{"class":195},[49,9352,324],{"class":206},[10,9354,9355,9357,9358,9360,9361,9363,9364,9366],{},[14,9356,9228],{}," lets you swap flagcdn.com for your own CDN; useful if you have a CSP that won't allow third-party images. ",[14,9359,9260],{}," lets you replace substring-matching with prefix-matching, fuzzy-matching, Arabic-name matching, whatever your users actually want. ",[14,9362,9290],{}," lets you ship your own curated list and skip the REST Countries fetch entirely; useful if you've already decided you only serve, say, Gulf countries. ",[14,9365,9339],{}," is i18n: pass an Arabic table and the validation reasons render in Arabic.",[10,9368,9369],{},"Every prop has a sensible default. The zero-config call still works.",[29,9371,9373],{"id":9372},"the-synchronous-dial-table-and-why-its-not-optional","The synchronous dial table, and why it's not optional",[10,9375,9376,9377,9380,9381,283,9384,9387,9388,9391,9392,9395,9396,7613,9398,9400],{},"The first time the component mounts in a fresh tab, the REST Countries fetch hasn't completed. Whatever indices ",[14,9378,9379],{},"usePhoneValidation"," builds (",[14,9382,9383],{},"byValue",[14,9385,9386],{},"byDialDigits",") are empty maps. If the consumer wrote ",[14,9389,9390],{},"default-country=\"20\""," in their template, expecting the picker to render with Egypt selected, what they'd get without help is an empty picker. The dial-digits index would return ",[14,9393,9394],{},"[]",", the watcher would set ",[14,9397,6053],{},[14,9399,5971],{},", and Egypt wouldn't appear until the network call returned.",[10,9402,9403],{},"So the component bakes a synchronous fallback table:",[40,9405,9407],{"className":186,"code":9406,"language":188,"meta":45,"style":45},"const DIAL_TO_ISO2_FALLBACK: Record\u003Cstring, string> = {\n  '1': 'US',\n  '7': 'RU',\n  '20': 'EG',\n  '27': 'ZA',\n  '33': 'FR',\n  '44': 'GB',\n  '49': 'DE',\n  '52': 'MX',\n  '55': 'BR',\n  '61': 'AU',\n  '81': 'JP',\n  '86': 'CN',\n  '91': 'IN',\n  '212': 'MA',\n  '966': 'SA',\n  '971': 'AE',\n  \u002F\u002F ... ~50 entries\n};\n",[14,9408,9409,9434,9446,9458,9470,9482,9494,9506,9518,9530,9542,9554,9566,9578,9590,9602,9614,9626,9631],{"__ignoreMap":45},[49,9410,9411,9413,9416,9418,9420,9422,9424,9426,9428,9430,9432],{"class":51,"line":52},[49,9412,951],{"class":195},[49,9414,9415],{"class":216}," DIAL_TO_ISO2_FALLBACK",[49,9417,229],{"class":195},[49,9419,275],{"class":202},[49,9421,207],{"class":206},[49,9423,280],{"class":216},[49,9425,283],{"class":206},[49,9427,280],{"class":216},[49,9429,289],{"class":206},[49,9431,638],{"class":195},[49,9433,967],{"class":206},[49,9435,9436,9439,9441,9444],{"class":51,"line":58},[49,9437,9438],{"class":310},"  '1'",[49,9440,5597],{"class":206},[49,9442,9443],{"class":310},"'US'",[49,9445,998],{"class":206},[49,9447,9448,9451,9453,9456],{"class":51,"line":64},[49,9449,9450],{"class":310},"  '7'",[49,9452,5597],{"class":206},[49,9454,9455],{"class":310},"'RU'",[49,9457,998],{"class":206},[49,9459,9460,9463,9465,9468],{"class":51,"line":70},[49,9461,9462],{"class":310},"  '20'",[49,9464,5597],{"class":206},[49,9466,9467],{"class":310},"'EG'",[49,9469,998],{"class":206},[49,9471,9472,9475,9477,9480],{"class":51,"line":76},[49,9473,9474],{"class":310},"  '27'",[49,9476,5597],{"class":206},[49,9478,9479],{"class":310},"'ZA'",[49,9481,998],{"class":206},[49,9483,9484,9487,9489,9492],{"class":51,"line":82},[49,9485,9486],{"class":310},"  '33'",[49,9488,5597],{"class":206},[49,9490,9491],{"class":310},"'FR'",[49,9493,998],{"class":206},[49,9495,9496,9499,9501,9504],{"class":51,"line":88},[49,9497,9498],{"class":310},"  '44'",[49,9500,5597],{"class":206},[49,9502,9503],{"class":310},"'GB'",[49,9505,998],{"class":206},[49,9507,9508,9511,9513,9516],{"class":51,"line":94},[49,9509,9510],{"class":310},"  '49'",[49,9512,5597],{"class":206},[49,9514,9515],{"class":310},"'DE'",[49,9517,998],{"class":206},[49,9519,9520,9523,9525,9528],{"class":51,"line":100},[49,9521,9522],{"class":310},"  '52'",[49,9524,5597],{"class":206},[49,9526,9527],{"class":310},"'MX'",[49,9529,998],{"class":206},[49,9531,9532,9535,9537,9540],{"class":51,"line":106},[49,9533,9534],{"class":310},"  '55'",[49,9536,5597],{"class":206},[49,9538,9539],{"class":310},"'BR'",[49,9541,998],{"class":206},[49,9543,9544,9547,9549,9552],{"class":51,"line":112},[49,9545,9546],{"class":310},"  '61'",[49,9548,5597],{"class":206},[49,9550,9551],{"class":310},"'AU'",[49,9553,998],{"class":206},[49,9555,9556,9559,9561,9564],{"class":51,"line":119},[49,9557,9558],{"class":310},"  '81'",[49,9560,5597],{"class":206},[49,9562,9563],{"class":310},"'JP'",[49,9565,998],{"class":206},[49,9567,9568,9571,9573,9576],{"class":51,"line":125},[49,9569,9570],{"class":310},"  '86'",[49,9572,5597],{"class":206},[49,9574,9575],{"class":310},"'CN'",[49,9577,998],{"class":206},[49,9579,9580,9583,9585,9588],{"class":51,"line":131},[49,9581,9582],{"class":310},"  '91'",[49,9584,5597],{"class":206},[49,9586,9587],{"class":310},"'IN'",[49,9589,998],{"class":206},[49,9591,9592,9595,9597,9600],{"class":51,"line":137},[49,9593,9594],{"class":310},"  '212'",[49,9596,5597],{"class":206},[49,9598,9599],{"class":310},"'MA'",[49,9601,998],{"class":206},[49,9603,9604,9607,9609,9612],{"class":51,"line":143},[49,9605,9606],{"class":310},"  '966'",[49,9608,5597],{"class":206},[49,9610,9611],{"class":310},"'SA'",[49,9613,998],{"class":206},[49,9615,9616,9619,9621,9624],{"class":51,"line":149},[49,9617,9618],{"class":310},"  '971'",[49,9620,5597],{"class":206},[49,9622,9623],{"class":310},"'AE'",[49,9625,998],{"class":206},[49,9627,9628],{"class":51,"line":155},[49,9629,9630],{"class":1852},"  \u002F\u002F ... ~50 entries\n",[49,9632,9633],{"class":51,"line":161},[49,9634,1318],{"class":206},[10,9636,9637,9638,9641],{},"And ",[14,9639,9640],{},"resolveCountryIdentifier"," checks the loaded index first, then this table:",[40,9643,9645],{"className":186,"code":9644,"language":188,"meta":45,"style":45},"function resolveCountryIdentifier(raw: string | undefined | null): string {\n  const v = String(raw ?? '').trim();\n  if (!v) return '';\n  if (\u002F^[A-Za-z]{2}$\u002F.test(v)) return v.toUpperCase();\n  const dial = v.replace(\u002F^\\+\u002F, '');\n  if (!\u002F^\\d+$\u002F.test(dial)) return '';\n  const match = getCountriesByDial(dial)[0];\n  if (match) return match.value;\n  return DIAL_TO_ISO2_FALLBACK[dial] ?? '';\n}\n",[14,9646,9647,9678,9703,9720,9753,9785,9818,9836,9848,9863],{"__ignoreMap":45},[49,9648,9649,9651,9653,9655,9658,9660,9662,9664,9666,9668,9670,9672,9674,9676],{"class":51,"line":52},[49,9650,1894],{"class":195},[49,9652,6156],{"class":202},[49,9654,1072],{"class":206},[49,9656,9657],{"class":225},"raw",[49,9659,229],{"class":195},[49,9661,232],{"class":216},[49,9663,235],{"class":195},[49,9665,1751],{"class":216},[49,9667,235],{"class":195},[49,9669,4657],{"class":216},[49,9671,1642],{"class":206},[49,9673,229],{"class":195},[49,9675,232],{"class":216},[49,9677,967],{"class":206},[49,9679,9680,9682,9685,9687,9689,9692,9694,9696,9698,9701],{"class":51,"line":58},[49,9681,1438],{"class":195},[49,9683,9684],{"class":216}," v",[49,9686,213],{"class":195},[49,9688,1960],{"class":202},[49,9690,9691],{"class":206},"(raw ",[49,9693,6493],{"class":195},[49,9695,6111],{"class":310},[49,9697,1669],{"class":206},[49,9699,9700],{"class":202},"trim",[49,9702,1998],{"class":206},[49,9704,9705,9707,9709,9711,9714,9716,9718],{"class":51,"line":64},[49,9706,1269],{"class":195},[49,9708,419],{"class":206},[49,9710,1274],{"class":195},[49,9712,9713],{"class":206},"v) ",[49,9715,1280],{"class":195},[49,9717,6111],{"class":310},[49,9719,324],{"class":206},[49,9721,9722,9724,9726,9728,9730,9733,9735,9737,9739,9741,9744,9746,9749,9751],{"class":51,"line":70},[49,9723,1269],{"class":195},[49,9725,419],{"class":206},[49,9727,7913],{"class":310},[49,9729,7904],{"class":195},[49,9731,9732],{"class":216},"[A-Za-z]",[49,9734,7910],{"class":195},[49,9736,7913],{"class":310},[49,9738,180],{"class":206},[49,9740,7918],{"class":202},[49,9742,9743],{"class":206},"(v)) ",[49,9745,1280],{"class":195},[49,9747,9748],{"class":206}," v.",[49,9750,7436],{"class":202},[49,9752,1998],{"class":206},[49,9754,9755,9757,9760,9762,9764,9767,9769,9771,9773,9777,9779,9781,9783],{"class":51,"line":76},[49,9756,1438],{"class":195},[49,9758,9759],{"class":216}," dial",[49,9761,213],{"class":195},[49,9763,9748],{"class":206},[49,9765,9766],{"class":202},"replace",[49,9768,1072],{"class":206},[49,9770,7913],{"class":310},[49,9772,7904],{"class":195},[49,9774,9776],{"class":9775},"snhLl","\\+",[49,9778,7913],{"class":310},[49,9780,283],{"class":206},[49,9782,5971],{"class":310},[49,9784,1970],{"class":206},[49,9786,9787,9789,9791,9793,9795,9797,9800,9803,9805,9807,9809,9812,9814,9816],{"class":51,"line":82},[49,9788,1269],{"class":195},[49,9790,419],{"class":206},[49,9792,1274],{"class":195},[49,9794,7913],{"class":310},[49,9796,7904],{"class":195},[49,9798,9799],{"class":216},"\\d",[49,9801,9802],{"class":195},"+$",[49,9804,7913],{"class":310},[49,9806,180],{"class":206},[49,9808,7918],{"class":202},[49,9810,9811],{"class":206},"(dial)) ",[49,9813,1280],{"class":195},[49,9815,6111],{"class":310},[49,9817,324],{"class":206},[49,9819,9820,9822,9824,9826,9828,9831,9833],{"class":51,"line":88},[49,9821,1438],{"class":195},[49,9823,7061],{"class":216},[49,9825,213],{"class":195},[49,9827,6787],{"class":202},[49,9829,9830],{"class":206},"(dial)[",[49,9832,6771],{"class":216},[49,9834,9835],{"class":206},"];\n",[49,9837,9838,9840,9843,9845],{"class":51,"line":94},[49,9839,1269],{"class":195},[49,9841,9842],{"class":206}," (match) ",[49,9844,1280],{"class":195},[49,9846,9847],{"class":206}," match.value;\n",[49,9849,9850,9852,9854,9857,9859,9861],{"class":51,"line":100},[49,9851,972],{"class":195},[49,9853,9415],{"class":216},[49,9855,9856],{"class":206},"[dial] ",[49,9858,6493],{"class":195},[49,9860,6111],{"class":310},[49,9862,324],{"class":206},[49,9864,9865],{"class":51,"line":106},[49,9866,169],{"class":206},[10,9868,9869],{},"The reverse, going from an ISO2 back to a dial number for the v-model, does the same check:",[40,9871,9873],{"className":186,"code":9872,"language":188,"meta":45,"style":45},"function dialNumberFor(iso2: string): number | null {\n  if (!iso2) return null;\n  const fromIndex = getCountryByValue(iso2)?.raw_data?.dial_digits;\n  const digits = fromIndex ?? Object.entries(DIAL_TO_ISO2_FALLBACK).find(([, v]) => v === iso2)?.[0];\n  if (!digits) return null;\n  const n = Number(digits);\n  return Number.isFinite(n) ? n : null;\n}\n",[14,9874,9875,9902,9919,9933,9984,10000,10015,10039],{"__ignoreMap":45},[49,9876,9877,9879,9882,9884,9886,9888,9890,9892,9894,9896,9898,9900],{"class":51,"line":52},[49,9878,1894],{"class":195},[49,9880,9881],{"class":202}," dialNumberFor",[49,9883,1072],{"class":206},[49,9885,9235],{"class":225},[49,9887,229],{"class":195},[49,9889,232],{"class":216},[49,9891,1642],{"class":206},[49,9893,229],{"class":195},[49,9895,444],{"class":216},[49,9897,235],{"class":195},[49,9899,4657],{"class":216},[49,9901,967],{"class":206},[49,9903,9904,9906,9908,9910,9913,9915,9917],{"class":51,"line":58},[49,9905,1269],{"class":195},[49,9907,419],{"class":206},[49,9909,1274],{"class":195},[49,9911,9912],{"class":206},"iso2) ",[49,9914,1280],{"class":195},[49,9916,4657],{"class":216},[49,9918,324],{"class":206},[49,9920,9921,9923,9926,9928,9930],{"class":51,"line":64},[49,9922,1438],{"class":195},[49,9924,9925],{"class":216}," fromIndex",[49,9927,213],{"class":195},[49,9929,6467],{"class":202},[49,9931,9932],{"class":206},"(iso2)?.raw_data?.dial_digits;\n",[49,9934,9935,9937,9940,9942,9945,9947,9949,9952,9954,9957,9959,9962,9965,9967,9970,9972,9975,9977,9980,9982],{"class":51,"line":70},[49,9936,1438],{"class":195},[49,9938,9939],{"class":216}," digits",[49,9941,213],{"class":195},[49,9943,9944],{"class":206}," fromIndex ",[49,9946,6493],{"class":195},[49,9948,1986],{"class":206},[49,9950,9951],{"class":202},"entries",[49,9953,1072],{"class":206},[49,9955,9956],{"class":216},"DIAL_TO_ISO2_FALLBACK",[49,9958,1669],{"class":206},[49,9960,9961],{"class":202},"find",[49,9963,9964],{"class":206},"(([, ",[49,9966,8797],{"class":225},[49,9968,9969],{"class":206},"]) ",[49,9971,361],{"class":195},[49,9973,9974],{"class":206}," v ",[49,9976,1296],{"class":195},[49,9978,9979],{"class":206}," iso2)?.[",[49,9981,6771],{"class":216},[49,9983,9835],{"class":206},[49,9985,9986,9988,9990,9992,9994,9996,9998],{"class":51,"line":76},[49,9987,1269],{"class":195},[49,9989,419],{"class":206},[49,9991,1274],{"class":195},[49,9993,6397],{"class":206},[49,9995,1280],{"class":195},[49,9997,4657],{"class":216},[49,9999,324],{"class":206},[49,10001,10002,10004,10007,10009,10012],{"class":51,"line":82},[49,10003,1438],{"class":195},[49,10005,10006],{"class":216}," n",[49,10008,213],{"class":195},[49,10010,10011],{"class":202}," Number",[49,10013,10014],{"class":206},"(digits);\n",[49,10016,10017,10019,10022,10025,10028,10030,10033,10035,10037],{"class":51,"line":88},[49,10018,972],{"class":195},[49,10020,10021],{"class":206}," Number.",[49,10023,10024],{"class":202},"isFinite",[49,10026,10027],{"class":206},"(n) ",[49,10029,986],{"class":195},[49,10031,10032],{"class":206}," n ",[49,10034,229],{"class":195},[49,10036,4657],{"class":216},[49,10038,324],{"class":206},[49,10040,10041],{"class":51,"line":94},[49,10042,169],{"class":206},[10,10044,10045,10046,10049],{},"The table is a known compromise. Fifty entries can't cover every dial code, and the choice of which fifty is opinionated: the most-populated countries plus the ones I personally need. A consumer who passes ",[14,10047,10048],{},"default-country=\"678\""," (Vanuatu) at mount with no cached country data will get nothing back from this lookup. A few seconds later, when the network call returns, the picker will resolve correctly. The fallback exists to make the 95% case work without a flicker; the long tail still ends up correct.",[10,10051,10052],{},"In-app, you can wait for the fetch. As a library, you can't make every consumer's mount block on a third-party API, and you can't ship 80KB of country data eagerly either. The sync table is the third path.",[29,10054,10056],{"id":10055},"recents-as-the-tiebreaker","Recents as the tiebreaker",[10,10058,10059,10060,10062],{},"The longest-prefix Tier 3 match isn't always unambiguous. ",[14,10061,6039],{}," matches the entire NANP block, 25-plus countries. Alphabetical-first would mean \"American Samoa\" or whichever country sorts earliest every time. That's not a useful default for anyone.",[10,10064,10065,10066,229],{},"So Tier 3 reads from ",[14,10067,5156],{},[40,10069,10071],{"className":186,"code":10070,"language":188,"meta":45,"style":45},"const recents = readRecents();\nconst recentHit = recents\n  .map((iso2) => group.find((c) => c.value === iso2))\n  .find((c): c is CountryOption => Boolean(c));\nif (recentHit) return { country: recentHit, nationalNumber };\nreturn { country: group[0], nationalNumber };\n",[14,10072,10073,10087,10099,10136,10166,10178],{"__ignoreMap":45},[49,10074,10075,10077,10080,10082,10085],{"class":51,"line":52},[49,10076,951],{"class":195},[49,10078,10079],{"class":216}," recents",[49,10081,213],{"class":195},[49,10083,10084],{"class":202}," readRecents",[49,10086,1998],{"class":206},[49,10088,10089,10091,10094,10096],{"class":51,"line":58},[49,10090,951],{"class":195},[49,10092,10093],{"class":216}," recentHit",[49,10095,213],{"class":195},[49,10097,10098],{"class":206}," recents\n",[49,10100,10101,10104,10106,10108,10110,10112,10114,10117,10119,10121,10124,10126,10128,10131,10133],{"class":51,"line":64},[49,10102,10103],{"class":206},"  .",[49,10105,2013],{"class":202},[49,10107,1675],{"class":206},[49,10109,9235],{"class":225},[49,10111,439],{"class":206},[49,10113,361],{"class":195},[49,10115,10116],{"class":206}," group.",[49,10118,9961],{"class":202},[49,10120,1675],{"class":206},[49,10122,10123],{"class":225},"c",[49,10125,439],{"class":206},[49,10127,361],{"class":195},[49,10129,10130],{"class":206}," c.value ",[49,10132,1296],{"class":195},[49,10134,10135],{"class":206}," iso2))\n",[49,10137,10138,10140,10142,10144,10146,10148,10150,10153,10156,10158,10160,10163],{"class":51,"line":70},[49,10139,10103],{"class":206},[49,10141,9961],{"class":202},[49,10143,1675],{"class":206},[49,10145,10123],{"class":225},[49,10147,1642],{"class":206},[49,10149,229],{"class":195},[49,10151,10152],{"class":225}," c",[49,10154,10155],{"class":195}," is",[49,10157,8555],{"class":202},[49,10159,1649],{"class":195},[49,10161,10162],{"class":202}," Boolean",[49,10164,10165],{"class":206},"(c));\n",[49,10167,10168,10170,10173,10175],{"class":51,"line":76},[49,10169,2487],{"class":195},[49,10171,10172],{"class":206}," (recentHit) ",[49,10174,1280],{"class":195},[49,10176,10177],{"class":206}," { country: recentHit, nationalNumber };\n",[49,10179,10180,10182,10184,10186],{"class":51,"line":82},[49,10181,1280],{"class":195},[49,10183,6853],{"class":206},[49,10185,6771],{"class":216},[49,10187,6858],{"class":206},[10,10189,10190,10191,10194,10195,10197],{},"The picker writes to ",[14,10192,10193],{},"ali_ui_country_recents_v1"," whenever the user explicitly picks a country, and the matcher reads it back during ambiguity. If you live in Canada and you've used the input before, the first ",[14,10196,5927],{}," you type will resolve to Canada. If you've never used it, you get the alphabetical fallback: still wrong, but only once.",[10,10199,10200],{},"A small touch, but it's the kind of thing that turns \"this technically works\" into \"this feels like it was built by someone who's used it.\"",[29,10202,10204],{"id":10203},"what-the-library-rewrite-was-really-about","What the library rewrite was really about",[10,10206,10207,10208,10211,10212,10215],{},"The original was about correctness: cursor jumps, validation reasons, bidi handling, the seven specific failure modes a phone input has to distinguish. None of that went away. All of it is still in this version: the ",[14,10209,10210],{},"dropLeadingZeros"," setter, the seven ",[14,10213,10214],{},"PhoneValidationReason"," enum values, the LTR-wrapper-over-RTL-input bidi pattern. Those things weren't wrong; they were the foundation.",[10,10217,10218,10219,10222],{},"What the library rewrite added was everything that's specific to ",[503,10220,10221],{},"not being one app",". An ISO2\u002Fdial split that survives ambiguous dial codes. An environment-detection chain that's both pluggable and offline-safe. A picker that's a popover on a laptop and a drawer on a phone, with separate class hooks for each. Fourteen slots so the consumer's design system isn't fighting the component's. A synchronous dial table because library consumers can't wait for the network. Recents as a tiebreaker because alphabetical-first is what \"lazy\" looks like.",[10,10224,10225],{},"The line count roughly doubled. The original was 160 lines of component plus 540 of composable; the new one is several hundred lines across two components and two composables. The surface area more than doubled. And the no-config call still fits in three lines:",[40,10227,10229],{"className":587,"code":10228,"language":589,"meta":45,"style":45},"\u003CATellInput v-model:phone=\"phone\" v-model:country=\"country\" show-validation \u002F>\n",[14,10230,10231],{"__ignoreMap":45},[49,10232,10233,10235,10237,10239,10241,10243,10245,10247,10249,10251,10253,10255,10257,10259,10261,10263,10265,10267],{"class":51,"line":52},[49,10234,207],{"class":206},[49,10236,5824],{"class":598},[49,10238,8182],{"class":202},[49,10240,229],{"class":206},[49,10242,8084],{"class":202},[49,10244,638],{"class":206},[49,10246,3703],{"class":310},[49,10248,8084],{"class":206},[49,10250,3703],{"class":310},[49,10252,8182],{"class":202},[49,10254,229],{"class":206},[49,10256,5910],{"class":202},[49,10258,638],{"class":206},[49,10260,3703],{"class":310},[49,10262,5910],{"class":206},[49,10264,3703],{"class":310},[49,10266,9120],{"class":202},[49,10268,1194],{"class":206},[10,10270,10271],{},"Every new prop has a default that mirrors what the in-app version did by accident. Consumers who want \"just a phone input\" get one. Consumers who want \"phone input but with our flags, our country list, our search predicate, our error messages\" get all the hooks they need.",[10,10273,10274,10275,10278,10279,10282],{},"The lesson I'd carry into the next one: making a component publishable is mostly about identifying the opinions you baked in for yourself and turning them into seams. Every ",[14,10276,10277],{},"restcountries.com"," hardcoded, every Saudi-and-Egypt fallback, every always-visible dropdown: those weren't wrong ",[503,10280,10281],{},"for the app",", but they were the things that wouldn't survive the next consumer. Find them, name them, replace each one with a default plus an override. The default keeps your old consumer happy; the override lets the new one in.",[4010,10284,10285],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html pre.shiki code .snhLl, html code.shiki .snhLl{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}",{"title":45,"searchDepth":58,"depth":58,"links":10287},[10288,10289,10290,10291,10292,10293,10294,10295],{"id":5841,"depth":58,"text":5842},{"id":6225,"depth":58,"text":6226},{"id":7235,"depth":58,"text":7236},{"id":8157,"depth":58,"text":8158},{"id":8303,"depth":58,"text":8304},{"id":9372,"depth":58,"text":9373},{"id":10055,"depth":58,"text":10056},{"id":10203,"depth":58,"text":10204},"2026-05-21","Turning an in-app phone input into a published Vue component forced specific changes. Detection-first UX, an ISO2 and dial-number split, a responsive popover and drawer picker, and slots for everything.","تحويل حقل هاتف من داخل تطبيق واحد إلى مكوّن Vue مُنشَر استوجب تغييرات جوهرية. تجربة استخدام تكتشف الدولة قبل أيّ تدخّل، وفصل بين رمز ISO2 ورقم الاتصال، ومنتقي متجاوب عبر Popover وDrawer، وSlots لكل شيء.",[5801,5807,10300,10301,10302,10303,10304,10305,10306,5806],"phone input","libphonenumber-js","country detection","responsive popover","vaul-vue","defineModel","slots",{},"\u002Fwrites\u002Fatell-input-the-library-rewrite",{"title":5817,"description":10297},"writes\u002Fatell-input-the-library-rewrite","ملاحظات من إعادة كتابة ATellInput كمكتبة مُنشَرة","U2nw1NwLneEoQyPNZeM61sua6if230l-WTmnZI7oQVY",1793901269153]