[{"data":1,"prerenderedAt":11351},["ShallowReactive",2],{"writes-\u002Fwrites\u002Fupload-and-download-progress-in-the-browser":3,"related-writes-\u002Fwrites\u002Fupload-and-download-progress-in-the-browser":5145},{"id":4,"title":5,"body":6,"date":5122,"description":5123,"description_ar":5124,"draft":5125,"extension":5126,"image":5127,"keywords":5128,"lang":5138,"meta":5139,"navigation":542,"path":5140,"seo":5141,"stem":5142,"title_ar":5143,"updatedAt":5122,"__hash__":5144},"writes\u002Fwrites\u002Fupload-and-download-progress-in-the-browser.md","A custom fetch wrapper, and XHR for upload progress",{"type":7,"value":8,"toc":5098},"minimark",[9,30,50,61,64,86,93,100,103,129,145,165,189,221,234,242,246,262,265,270,273,441,444,447,734,748,755,760,804,807,1008,1029,1033,1036,1617,1624,1627,1701,1704,1778,1782,1785,2005,2012,2016,2019,2156,2161,2164,2171,2177,2343,2349,2369,2372,2376,2379,2527,2530,2551,2564,2577,2588,2597,2658,2693,2696,2805,2814,2818,2821,3222,3225,3268,3272,3278,3617,3640,3658,3664,3673,3685,3688,4508,4511,4523,4542,4760,4781,4815,4819,4834,4919,4932,4951,4955,4958,4971,4985,4996,5011,5028,5032,5035,5091,5094],[10,11,12,13,17,18,21,22,25,26,29],"p",{},"Anytime I need to talk to an HTTP API from the browser, I reach for ",[14,15,16],"code",{},"fetch"," first. It's the right primitive: Promise-first, composable with ",[14,19,20],{},"AbortController",", shares ",[14,23,24],{},"Request","\u002F",[14,27,28],{},"Response"," with Service Workers and the Cache API, and the same signature runs in Node 18+, Bun, Deno, and Cloudflare Workers without an adapter. Most \"which HTTP client should I use?\" conversations end there.",[10,31,32,33,35,36,38,39,41,42,45,46,49],{},"But ",[14,34,16],{}," has one gap that nobody warns you about, and it's not the kind of gap someone's going to fix. It's structural. ",[14,37,16],{}," doesn't emit upload progress events. The ",[14,40,24],{}," body can be a ",[14,43,44],{},"ReadableStream",", but browsers don't fire ",[14,47,48],{},"progress"," as they drain bytes out of it onto the wire. You hand the body to the network stack and it goes.",[10,51,52,53,56,57,60],{},"For a real upload progress bar (not a fake indeterminate spinner) you need ",[14,54,55],{},"XMLHttpRequest",". It's the older API, but it's the only one that exposes ",[14,58,59],{},"xhr.upload.onprogress",".",[10,62,63],{},"This post covers three things, in order:",[65,66,67,74,80],"ol",{},[68,69,70,71,73],"li",{},"Why native ",[14,72,16],{}," earns its default status, and the specific benefits you're getting from it.",[68,75,76,77,79],{},"How to build a custom fetch wrapper (defaults, timeouts, interceptors, retry) without losing ",[14,78,16],{},"'s shape.",[68,81,82,83,85],{},"The XHR gap, the progress event map, and a small XHR-backed wrapper that returns a ",[14,84,28],{}," so the rest of your code stays fetch-shaped.",[10,87,88,89,92],{},"If you've ever wondered why so many production codebases have a ",[14,90,91],{},"createClient()"," factory, the answer mostly lives in parts 1 and 2. Part 3 is the single thing that client still can't do on its own.",[94,95,70,97,99],"h2",{"id":96},"why-native-fetch-is-the-default-primitive",[14,98,16],{}," is the default primitive",[10,101,102],{},"Every HTTP-client debate I've been in eventually converges on \"use fetch.\" The reasons are worth stating outright. They're what you'd be giving up by defaulting to a different library, and they compound once you start wrapping it.",[10,104,105,109,110,113,114,117,118,121,122,125,126,128],{},[106,107,108],"strong",{},"One Promise, one Response."," ",[14,111,112],{},"fetch(url, init)"," returns a ",[14,115,116],{},"Promise\u003CResponse>",". No state machine, no ",[14,119,120],{},"readyState",", no event-handler wiring for the happy path. ",[14,123,124],{},"await fetch(...)"," reads like every other async call in your codebase. XHR is still event-driven under the hood; ",[14,127,16],{}," is the Promise-shaped interface.",[10,130,131,134,135,137,138,140,141,144],{},[106,132,133],{},"Unified Request\u002FResponse primitives."," The ",[14,136,28],{}," you get from ",[14,139,16],{}," is the same type Service Workers return, the Cache API stores, and edge runtimes accept. You can ",[14,142,143],{},".clone()"," a response, hand it back to the browser, cache it, or pipe it elsewhere: no translation layer.",[10,146,147,152,153,156,157,160,161,164],{},[106,148,149,150,60],{},"Cancellation via ",[14,151,20],{}," One signal can cancel a whole chain of work: a fetch, a downstream computation, a retry that hasn't fired yet. ",[14,154,155],{},"AbortSignal.any([...])"," composes multiple signals into one; ",[14,158,159],{},"AbortSignal.timeout(ms)"," gives you a timeout without a ",[14,162,163],{},"setTimeout\u002FclearTimeout"," dance. Cancellation is a platform feature, not a library concern.",[10,166,167,109,170,173,174,176,177,180,181,184,185,188],{},[106,168,169],{},"Streaming bodies.",[14,171,172],{},"Response.body"," is a ",[14,175,44],{},". For downloads, you can read chunks as they arrive: enough for a bytes-loaded counter, a live parser, or handing the stream into something else (",[14,178,179],{},"createImageBitmap",", ",[14,182,183],{},"new Response(stream)",", the Cache API). ",[14,186,187],{},"Request.body"," accepts streams too, though upload progress is the one place this doesn't translate.",[10,190,191,109,194,180,197,180,200,180,203,180,206,180,209,212,213,216,217,220],{},[106,192,193],{},"First-class request options.",[14,195,196],{},"credentials",[14,198,199],{},"cache",[14,201,202],{},"redirect",[14,204,205],{},"mode",[14,207,208],{},"referrer",[14,210,211],{},"integrity",": all top-level ",[14,214,215],{},"RequestInit"," fields. No ",[14,218,219],{},"xhr.withCredentials = true"," side-effect, no forbidden-header workarounds. The API was designed with modern security and caching semantics in mind.",[10,222,223,226,227,229,230,233],{},[106,224,225],{},"Cross-runtime consistency."," The same ",[14,228,16],{}," signature runs in browsers, Node 18+, Bun, Deno, Cloudflare Workers, and Service Workers. An SSR-capable client doesn't need a ",[14,231,232],{},"typeof window"," branch. Server-rendered code and client-rendered code can share one HTTP layer.",[10,235,236,109,239,241],{},[106,237,238],{},"Extensibility without class hierarchies.",[14,240,16],{}," is a plain function. Your custom client is also a plain function with the same signature. Wrapping, currying, proxying, replacing: all standard techniques apply. That's what makes the next section tractable.",[94,243,245],{"id":244},"building-a-custom-fetch-wrapper","Building a custom fetch wrapper",[10,247,248,249,251,252,254,255,258,259,261],{},"Most production codebases don't call ",[14,250,16],{}," directly. They have a ",[14,253,91],{}," or ",[14,256,257],{},"apiClient"," that layers on defaults, auth headers, timeouts, retry, error normalization, and sometimes interceptors. The trick is to build it without losing ",[14,260,16],{},"'s shape, so swapping in a different transport (like the XHR-backed one below) doesn't force a rewrite of every consumer.",[10,263,264],{},"I'll walk through four layers, each small, each earning its weight.",[266,267,269],"h3",{"id":268},"layer-1-base-url-default-headers-parsed-json","Layer 1: base URL, default headers, parsed JSON",[10,271,272],{},"Start with the shortest useful wrapper:",[274,275,280],"pre",{"className":276,"code":277,"language":278,"meta":279,"style":279},"language-typescript shiki shiki-themes github-light github-dark","export async function request\u003CT>(path: string, init: RequestInit = {}): Promise\u003CT> {\n  const res = await fetch(path, init);\n  if (!res.ok) throw new ApiError(res.status, await res.text());\n  return res.json() as Promise\u003CT>;\n}\n","typescript","",[14,281,282,350,370,409,435],{"__ignoreMap":279},[283,284,287,291,294,297,301,305,308,311,315,318,322,324,327,329,332,335,338,340,343,345,347],"span",{"class":285,"line":286},"line",1,[283,288,290],{"class":289},"szBVR","export",[283,292,293],{"class":289}," async",[283,295,296],{"class":289}," function",[283,298,300],{"class":299},"sScJk"," request",[283,302,304],{"class":303},"sVt8B","\u003C",[283,306,307],{"class":299},"T",[283,309,310],{"class":303},">(",[283,312,314],{"class":313},"s4XuR","path",[283,316,317],{"class":289},":",[283,319,321],{"class":320},"sj4cs"," string",[283,323,180],{"class":303},[283,325,326],{"class":313},"init",[283,328,317],{"class":289},[283,330,331],{"class":299}," RequestInit",[283,333,334],{"class":289}," =",[283,336,337],{"class":303}," {})",[283,339,317],{"class":289},[283,341,342],{"class":299}," Promise",[283,344,304],{"class":303},[283,346,307],{"class":299},[283,348,349],{"class":303},"> {\n",[283,351,353,356,359,361,364,367],{"class":285,"line":352},2,[283,354,355],{"class":289},"  const",[283,357,358],{"class":320}," res",[283,360,334],{"class":289},[283,362,363],{"class":289}," await",[283,365,366],{"class":299}," fetch",[283,368,369],{"class":303},"(path, init);\n",[283,371,373,376,379,382,385,388,391,394,397,400,403,406],{"class":285,"line":372},3,[283,374,375],{"class":289},"  if",[283,377,378],{"class":303}," (",[283,380,381],{"class":289},"!",[283,383,384],{"class":303},"res.ok) ",[283,386,387],{"class":289},"throw",[283,389,390],{"class":289}," new",[283,392,393],{"class":299}," ApiError",[283,395,396],{"class":303},"(res.status, ",[283,398,399],{"class":289},"await",[283,401,402],{"class":303}," res.",[283,404,405],{"class":299},"text",[283,407,408],{"class":303},"());\n",[283,410,412,415,417,420,423,426,428,430,432],{"class":285,"line":411},4,[283,413,414],{"class":289},"  return",[283,416,402],{"class":303},[283,418,419],{"class":299},"json",[283,421,422],{"class":303},"() ",[283,424,425],{"class":289},"as",[283,427,342],{"class":299},[283,429,304],{"class":303},[283,431,307],{"class":299},[283,433,434],{"class":303},">;\n",[283,436,438],{"class":285,"line":437},5,[283,439,440],{"class":303},"}\n",[10,442,443],{},"Three lines buy you two behaviors: \"throw on non-2xx\" and \"parse JSON on success.\" Once you find yourself repeating those at every call site, the wrapper earns its place.",[10,445,446],{},"A slightly more useful shape accepts configuration:",[274,448,450],{"className":276,"code":449,"language":278,"meta":279,"style":279},"export function createClient(options: {\n  baseURL: string;\n  defaultHeaders?: Record\u003Cstring, string>;\n}) {\n  const { baseURL, defaultHeaders = {} } = options;\n\n  return async function request\u003CT>(path: string, init: RequestInit = {}): Promise\u003CT> {\n    const url = new URL(path, baseURL).toString();\n    const headers = { ...defaultHeaders, ...(init.headers as Record\u003Cstring, string>) };\n    const res = await fetch(url, { ...init, headers });\n    if (!res.ok) throw new ApiError(res.status, await res.text());\n    return res.json() as Promise\u003CT>;\n  };\n}\n",[14,451,452,472,484,506,511,537,544,589,614,652,673,701,723,729],{"__ignoreMap":279},[283,453,454,456,458,461,464,467,469],{"class":285,"line":286},[283,455,290],{"class":289},[283,457,296],{"class":289},[283,459,460],{"class":299}," createClient",[283,462,463],{"class":303},"(",[283,465,466],{"class":313},"options",[283,468,317],{"class":289},[283,470,471],{"class":303}," {\n",[283,473,474,477,479,481],{"class":285,"line":352},[283,475,476],{"class":313},"  baseURL",[283,478,317],{"class":289},[283,480,321],{"class":320},[283,482,483],{"class":303},";\n",[283,485,486,489,492,495,497,500,502,504],{"class":285,"line":372},[283,487,488],{"class":313},"  defaultHeaders",[283,490,491],{"class":289},"?:",[283,493,494],{"class":299}," Record",[283,496,304],{"class":303},[283,498,499],{"class":320},"string",[283,501,180],{"class":303},[283,503,499],{"class":320},[283,505,434],{"class":303},[283,507,508],{"class":285,"line":411},[283,509,510],{"class":303},"}) {\n",[283,512,513,515,518,521,523,526,528,531,534],{"class":285,"line":437},[283,514,355],{"class":289},[283,516,517],{"class":303}," { ",[283,519,520],{"class":320},"baseURL",[283,522,180],{"class":303},[283,524,525],{"class":320},"defaultHeaders",[283,527,334],{"class":289},[283,529,530],{"class":303}," {} } ",[283,532,533],{"class":289},"=",[283,535,536],{"class":303}," options;\n",[283,538,540],{"class":285,"line":539},6,[283,541,543],{"emptyLinePlaceholder":542},true,"\n",[283,545,547,549,551,553,555,557,559,561,563,565,567,569,571,573,575,577,579,581,583,585,587],{"class":285,"line":546},7,[283,548,414],{"class":289},[283,550,293],{"class":289},[283,552,296],{"class":289},[283,554,300],{"class":299},[283,556,304],{"class":303},[283,558,307],{"class":299},[283,560,310],{"class":303},[283,562,314],{"class":313},[283,564,317],{"class":289},[283,566,321],{"class":320},[283,568,180],{"class":303},[283,570,326],{"class":313},[283,572,317],{"class":289},[283,574,331],{"class":299},[283,576,334],{"class":289},[283,578,337],{"class":303},[283,580,317],{"class":289},[283,582,342],{"class":299},[283,584,304],{"class":303},[283,586,307],{"class":299},[283,588,349],{"class":303},[283,590,592,595,598,600,602,605,608,611],{"class":285,"line":591},8,[283,593,594],{"class":289},"    const",[283,596,597],{"class":320}," url",[283,599,334],{"class":289},[283,601,390],{"class":289},[283,603,604],{"class":299}," URL",[283,606,607],{"class":303},"(path, baseURL).",[283,609,610],{"class":299},"toString",[283,612,613],{"class":303},"();\n",[283,615,617,619,622,624,626,629,632,634,637,639,641,643,645,647,649],{"class":285,"line":616},9,[283,618,594],{"class":289},[283,620,621],{"class":320}," headers",[283,623,334],{"class":289},[283,625,517],{"class":303},[283,627,628],{"class":289},"...",[283,630,631],{"class":303},"defaultHeaders, ",[283,633,628],{"class":289},[283,635,636],{"class":303},"(init.headers ",[283,638,425],{"class":289},[283,640,494],{"class":299},[283,642,304],{"class":303},[283,644,499],{"class":320},[283,646,180],{"class":303},[283,648,499],{"class":320},[283,650,651],{"class":303},">) };\n",[283,653,655,657,659,661,663,665,668,670],{"class":285,"line":654},10,[283,656,594],{"class":289},[283,658,358],{"class":320},[283,660,334],{"class":289},[283,662,363],{"class":289},[283,664,366],{"class":299},[283,666,667],{"class":303},"(url, { ",[283,669,628],{"class":289},[283,671,672],{"class":303},"init, headers });\n",[283,674,676,679,681,683,685,687,689,691,693,695,697,699],{"class":285,"line":675},11,[283,677,678],{"class":289},"    if",[283,680,378],{"class":303},[283,682,381],{"class":289},[283,684,384],{"class":303},[283,686,387],{"class":289},[283,688,390],{"class":289},[283,690,393],{"class":299},[283,692,396],{"class":303},[283,694,399],{"class":289},[283,696,402],{"class":303},[283,698,405],{"class":299},[283,700,408],{"class":303},[283,702,704,707,709,711,713,715,717,719,721],{"class":285,"line":703},12,[283,705,706],{"class":289},"    return",[283,708,402],{"class":303},[283,710,419],{"class":299},[283,712,422],{"class":303},[283,714,425],{"class":289},[283,716,342],{"class":299},[283,718,304],{"class":303},[283,720,307],{"class":299},[283,722,434],{"class":303},[283,724,726],{"class":285,"line":725},13,[283,727,728],{"class":303},"  };\n",[283,730,732],{"class":285,"line":731},14,[283,733,440],{"class":303},[10,735,736,739,740,743,744,747],{},[14,737,738],{},"new URL(path, baseURL)"," is the piece worth stealing. It handles leading and trailing slashes correctly without the manual ",[14,741,742],{},"${baseURL}\u002F${path}.replace(\u002F\\\u002F+\u002Fg, '\u002F')"," dance that every ad-hoc client eventually grows. Pass a relative path and it resolves against the base; pass an absolute URL and ",[14,745,746],{},"URL"," uses it as-is.",[266,749,751,752],{"id":750},"layer-2-timeouts-via-abortsignal","Layer 2: timeouts via ",[14,753,754],{},"AbortSignal",[10,756,757,758,317],{},"The right way to do timeouts is ",[14,759,159],{},[274,761,763],{"className":276,"code":762,"language":278,"meta":279,"style":279},"const signal = AbortSignal.timeout(30_000);\nconst res = await fetch(url, { signal });\n",[14,764,765,789],{"__ignoreMap":279},[283,766,767,770,773,775,778,781,783,786],{"class":285,"line":286},[283,768,769],{"class":289},"const",[283,771,772],{"class":320}," signal",[283,774,334],{"class":289},[283,776,777],{"class":303}," AbortSignal.",[283,779,780],{"class":299},"timeout",[283,782,463],{"class":303},[283,784,785],{"class":320},"30_000",[283,787,788],{"class":303},");\n",[283,790,791,793,795,797,799,801],{"class":285,"line":352},[283,792,769],{"class":289},[283,794,358],{"class":320},[283,796,334],{"class":289},[283,798,363],{"class":289},[283,800,366],{"class":299},[283,802,803],{"class":303},"(url, { signal });\n",[10,805,806],{},"If a caller also passes their own signal, merge them so either one can cancel the request:",[274,808,810],{"className":276,"code":809,"language":278,"meta":279,"style":279},"function mergeSignals(a?: AbortSignal, b?: AbortSignal): AbortSignal | undefined {\n  if (!a) return b;\n  if (!b) return a;\n  if ('any' in AbortSignal) return AbortSignal.any([a, b]);\n  \u002F\u002F fallback for older runtimes\n  const controller = new AbortController();\n  const onAbort = () => controller.abort();\n  a.addEventListener('abort', onAbort, { once: true });\n  b.addEventListener('abort', onAbort, { once: true });\n  return controller.signal;\n}\n",[14,811,812,854,871,887,913,919,935,958,980,997,1004],{"__ignoreMap":279},[283,813,814,817,820,822,825,827,830,832,835,837,839,842,844,846,849,852],{"class":285,"line":286},[283,815,816],{"class":289},"function",[283,818,819],{"class":299}," mergeSignals",[283,821,463],{"class":303},[283,823,824],{"class":313},"a",[283,826,491],{"class":289},[283,828,829],{"class":299}," AbortSignal",[283,831,180],{"class":303},[283,833,834],{"class":313},"b",[283,836,491],{"class":289},[283,838,829],{"class":299},[283,840,841],{"class":303},")",[283,843,317],{"class":289},[283,845,829],{"class":299},[283,847,848],{"class":289}," |",[283,850,851],{"class":320}," undefined",[283,853,471],{"class":303},[283,855,856,858,860,862,865,868],{"class":285,"line":352},[283,857,375],{"class":289},[283,859,378],{"class":303},[283,861,381],{"class":289},[283,863,864],{"class":303},"a) ",[283,866,867],{"class":289},"return",[283,869,870],{"class":303}," b;\n",[283,872,873,875,877,879,882,884],{"class":285,"line":372},[283,874,375],{"class":289},[283,876,378],{"class":303},[283,878,381],{"class":289},[283,880,881],{"class":303},"b) ",[283,883,867],{"class":289},[283,885,886],{"class":303}," a;\n",[283,888,889,891,893,897,900,903,905,907,910],{"class":285,"line":411},[283,890,375],{"class":289},[283,892,378],{"class":303},[283,894,896],{"class":895},"sZZnC","'any'",[283,898,899],{"class":289}," in",[283,901,902],{"class":303}," AbortSignal) ",[283,904,867],{"class":289},[283,906,777],{"class":303},[283,908,909],{"class":299},"any",[283,911,912],{"class":303},"([a, b]);\n",[283,914,915],{"class":285,"line":437},[283,916,918],{"class":917},"sJ8bj","  \u002F\u002F fallback for older runtimes\n",[283,920,921,923,926,928,930,933],{"class":285,"line":539},[283,922,355],{"class":289},[283,924,925],{"class":320}," controller",[283,927,334],{"class":289},[283,929,390],{"class":289},[283,931,932],{"class":299}," AbortController",[283,934,613],{"class":303},[283,936,937,939,942,944,947,950,953,956],{"class":285,"line":546},[283,938,355],{"class":289},[283,940,941],{"class":299}," onAbort",[283,943,334],{"class":289},[283,945,946],{"class":303}," () ",[283,948,949],{"class":289},"=>",[283,951,952],{"class":303}," controller.",[283,954,955],{"class":299},"abort",[283,957,613],{"class":303},[283,959,960,963,966,968,971,974,977],{"class":285,"line":591},[283,961,962],{"class":303},"  a.",[283,964,965],{"class":299},"addEventListener",[283,967,463],{"class":303},[283,969,970],{"class":895},"'abort'",[283,972,973],{"class":303},", onAbort, { once: ",[283,975,976],{"class":320},"true",[283,978,979],{"class":303}," });\n",[283,981,982,985,987,989,991,993,995],{"class":285,"line":616},[283,983,984],{"class":303},"  b.",[283,986,965],{"class":299},[283,988,463],{"class":303},[283,990,970],{"class":895},[283,992,973],{"class":303},[283,994,976],{"class":320},[283,996,979],{"class":303},[283,998,999,1001],{"class":285,"line":654},[283,1000,414],{"class":289},[283,1002,1003],{"class":303}," controller.signal;\n",[283,1005,1006],{"class":285,"line":675},[283,1007,440],{"class":303},[10,1009,1010,1012,1013,1015,1016,1020,1021,1024,1025,1028],{},[14,1011,155],{}," does the merge natively in recent browsers; the fallback manually forwards both signals into a new controller. Either way, a single signal flows into ",[14,1014,16],{},", and a timeout ",[1017,1018,1019],"em",{},"or"," a caller abort resolves to the same rejection path, which matters because downstream ",[14,1022,1023],{},"try\u002Fcatch"," can check ",[14,1026,1027],{},"err.name === 'AbortError'"," regardless of which source fired.",[266,1030,1032],{"id":1031},"layer-3-interceptors","Layer 3: interceptors",[10,1034,1035],{},"Three chains (request, response, error) turn a static client into an extensible one:",[274,1037,1039],{"className":276,"code":1038,"language":278,"meta":279,"style":279},"type RequestInterceptor = (ctx: { url: string; init: RequestInit }) => void | Promise\u003Cvoid>;\ntype ResponseInterceptor = (res: Response) => Response | Promise\u003CResponse>;\n\nexport function createClient(options: { baseURL: string; defaultHeaders?: Record\u003Cstring, string> }) {\n  const requestInterceptors: RequestInterceptor[] = [];\n  const responseInterceptors: ResponseInterceptor[] = [];\n\n  async function request\u003CT>(path: string, init: RequestInit = {}): Promise\u003CT> {\n    const ctx = {\n      url: new URL(path, options.baseURL).toString(),\n      init: { ...init, headers: { ...(options.defaultHeaders ?? {}), ...(init.headers as Record\u003Cstring, string>) } },\n    };\n    for (const fn of requestInterceptors) await fn(ctx);\n    let res = await fetch(ctx.url, ctx.init);\n    for (const fn of responseInterceptors) res = await fn(res);\n    if (!res.ok) throw new ApiError(res.status, await res.text());\n    return res.json() as Promise\u003CT>;\n  }\n\n  request.useRequest = (fn: RequestInterceptor) => {\n    requestInterceptors.push(fn);\n    return () => void requestInterceptors.splice(requestInterceptors.indexOf(fn), 1);\n  };\n  request.useResponse = (fn: ResponseInterceptor) => {\n    responseInterceptors.push(fn);\n    return () => void responseInterceptors.splice(responseInterceptors.indexOf(fn), 1);\n  };\n\n  return request;\n}\n",[14,1040,1041,1095,1131,1135,1176,1195,1212,1216,1259,1270,1288,1328,1333,1358,1375,1400,1427,1448,1454,1459,1485,1497,1528,1533,1557,1567,1594,1599,1604,1612],{"__ignoreMap":279},[283,1042,1043,1046,1049,1051,1053,1056,1058,1060,1063,1065,1067,1070,1072,1074,1076,1079,1081,1084,1086,1088,1090,1093],{"class":285,"line":286},[283,1044,1045],{"class":289},"type",[283,1047,1048],{"class":299}," RequestInterceptor",[283,1050,334],{"class":289},[283,1052,378],{"class":303},[283,1054,1055],{"class":313},"ctx",[283,1057,317],{"class":289},[283,1059,517],{"class":303},[283,1061,1062],{"class":313},"url",[283,1064,317],{"class":289},[283,1066,321],{"class":320},[283,1068,1069],{"class":303},"; ",[283,1071,326],{"class":313},[283,1073,317],{"class":289},[283,1075,331],{"class":299},[283,1077,1078],{"class":303}," }) ",[283,1080,949],{"class":289},[283,1082,1083],{"class":320}," void",[283,1085,848],{"class":289},[283,1087,342],{"class":299},[283,1089,304],{"class":303},[283,1091,1092],{"class":320},"void",[283,1094,434],{"class":303},[283,1096,1097,1099,1102,1104,1106,1109,1111,1114,1117,1119,1121,1123,1125,1127,1129],{"class":285,"line":352},[283,1098,1045],{"class":289},[283,1100,1101],{"class":299}," ResponseInterceptor",[283,1103,334],{"class":289},[283,1105,378],{"class":303},[283,1107,1108],{"class":313},"res",[283,1110,317],{"class":289},[283,1112,1113],{"class":299}," Response",[283,1115,1116],{"class":303},") ",[283,1118,949],{"class":289},[283,1120,1113],{"class":299},[283,1122,848],{"class":289},[283,1124,342],{"class":299},[283,1126,304],{"class":303},[283,1128,28],{"class":299},[283,1130,434],{"class":303},[283,1132,1133],{"class":285,"line":372},[283,1134,543],{"emptyLinePlaceholder":542},[283,1136,1137,1139,1141,1143,1145,1147,1149,1151,1153,1155,1157,1159,1161,1163,1165,1167,1169,1171,1173],{"class":285,"line":411},[283,1138,290],{"class":289},[283,1140,296],{"class":289},[283,1142,460],{"class":299},[283,1144,463],{"class":303},[283,1146,466],{"class":313},[283,1148,317],{"class":289},[283,1150,517],{"class":303},[283,1152,520],{"class":313},[283,1154,317],{"class":289},[283,1156,321],{"class":320},[283,1158,1069],{"class":303},[283,1160,525],{"class":313},[283,1162,491],{"class":289},[283,1164,494],{"class":299},[283,1166,304],{"class":303},[283,1168,499],{"class":320},[283,1170,180],{"class":303},[283,1172,499],{"class":320},[283,1174,1175],{"class":303},"> }) {\n",[283,1177,1178,1180,1183,1185,1187,1190,1192],{"class":285,"line":437},[283,1179,355],{"class":289},[283,1181,1182],{"class":320}," requestInterceptors",[283,1184,317],{"class":289},[283,1186,1048],{"class":299},[283,1188,1189],{"class":303},"[] ",[283,1191,533],{"class":289},[283,1193,1194],{"class":303}," [];\n",[283,1196,1197,1199,1202,1204,1206,1208,1210],{"class":285,"line":539},[283,1198,355],{"class":289},[283,1200,1201],{"class":320}," responseInterceptors",[283,1203,317],{"class":289},[283,1205,1101],{"class":299},[283,1207,1189],{"class":303},[283,1209,533],{"class":289},[283,1211,1194],{"class":303},[283,1213,1214],{"class":285,"line":546},[283,1215,543],{"emptyLinePlaceholder":542},[283,1217,1218,1221,1223,1225,1227,1229,1231,1233,1235,1237,1239,1241,1243,1245,1247,1249,1251,1253,1255,1257],{"class":285,"line":591},[283,1219,1220],{"class":289},"  async",[283,1222,296],{"class":289},[283,1224,300],{"class":299},[283,1226,304],{"class":303},[283,1228,307],{"class":299},[283,1230,310],{"class":303},[283,1232,314],{"class":313},[283,1234,317],{"class":289},[283,1236,321],{"class":320},[283,1238,180],{"class":303},[283,1240,326],{"class":313},[283,1242,317],{"class":289},[283,1244,331],{"class":299},[283,1246,334],{"class":289},[283,1248,337],{"class":303},[283,1250,317],{"class":289},[283,1252,342],{"class":299},[283,1254,304],{"class":303},[283,1256,307],{"class":299},[283,1258,349],{"class":303},[283,1260,1261,1263,1266,1268],{"class":285,"line":616},[283,1262,594],{"class":289},[283,1264,1265],{"class":320}," ctx",[283,1267,334],{"class":289},[283,1269,471],{"class":303},[283,1271,1272,1275,1278,1280,1283,1285],{"class":285,"line":654},[283,1273,1274],{"class":303},"      url: ",[283,1276,1277],{"class":289},"new",[283,1279,604],{"class":299},[283,1281,1282],{"class":303},"(path, options.baseURL).",[283,1284,610],{"class":299},[283,1286,1287],{"class":303},"(),\n",[283,1289,1290,1293,1295,1298,1300,1303,1306,1309,1311,1313,1315,1317,1319,1321,1323,1325],{"class":285,"line":675},[283,1291,1292],{"class":303},"      init: { ",[283,1294,628],{"class":289},[283,1296,1297],{"class":303},"init, headers: { ",[283,1299,628],{"class":289},[283,1301,1302],{"class":303},"(options.defaultHeaders ",[283,1304,1305],{"class":289},"??",[283,1307,1308],{"class":303}," {}), ",[283,1310,628],{"class":289},[283,1312,636],{"class":303},[283,1314,425],{"class":289},[283,1316,494],{"class":299},[283,1318,304],{"class":303},[283,1320,499],{"class":320},[283,1322,180],{"class":303},[283,1324,499],{"class":320},[283,1326,1327],{"class":303},">) } },\n",[283,1329,1330],{"class":285,"line":703},[283,1331,1332],{"class":303},"    };\n",[283,1334,1335,1338,1340,1342,1345,1348,1351,1353,1355],{"class":285,"line":725},[283,1336,1337],{"class":289},"    for",[283,1339,378],{"class":303},[283,1341,769],{"class":289},[283,1343,1344],{"class":320}," fn",[283,1346,1347],{"class":289}," of",[283,1349,1350],{"class":303}," requestInterceptors) ",[283,1352,399],{"class":289},[283,1354,1344],{"class":299},[283,1356,1357],{"class":303},"(ctx);\n",[283,1359,1360,1363,1366,1368,1370,1372],{"class":285,"line":731},[283,1361,1362],{"class":289},"    let",[283,1364,1365],{"class":303}," res ",[283,1367,533],{"class":289},[283,1369,363],{"class":289},[283,1371,366],{"class":299},[283,1373,1374],{"class":303},"(ctx.url, ctx.init);\n",[283,1376,1378,1380,1382,1384,1386,1388,1391,1393,1395,1397],{"class":285,"line":1377},15,[283,1379,1337],{"class":289},[283,1381,378],{"class":303},[283,1383,769],{"class":289},[283,1385,1344],{"class":320},[283,1387,1347],{"class":289},[283,1389,1390],{"class":303}," responseInterceptors) res ",[283,1392,533],{"class":289},[283,1394,363],{"class":289},[283,1396,1344],{"class":299},[283,1398,1399],{"class":303},"(res);\n",[283,1401,1403,1405,1407,1409,1411,1413,1415,1417,1419,1421,1423,1425],{"class":285,"line":1402},16,[283,1404,678],{"class":289},[283,1406,378],{"class":303},[283,1408,381],{"class":289},[283,1410,384],{"class":303},[283,1412,387],{"class":289},[283,1414,390],{"class":289},[283,1416,393],{"class":299},[283,1418,396],{"class":303},[283,1420,399],{"class":289},[283,1422,402],{"class":303},[283,1424,405],{"class":299},[283,1426,408],{"class":303},[283,1428,1430,1432,1434,1436,1438,1440,1442,1444,1446],{"class":285,"line":1429},17,[283,1431,706],{"class":289},[283,1433,402],{"class":303},[283,1435,419],{"class":299},[283,1437,422],{"class":303},[283,1439,425],{"class":289},[283,1441,342],{"class":299},[283,1443,304],{"class":303},[283,1445,307],{"class":299},[283,1447,434],{"class":303},[283,1449,1451],{"class":285,"line":1450},18,[283,1452,1453],{"class":303},"  }\n",[283,1455,1457],{"class":285,"line":1456},19,[283,1458,543],{"emptyLinePlaceholder":542},[283,1460,1462,1465,1468,1470,1472,1475,1477,1479,1481,1483],{"class":285,"line":1461},20,[283,1463,1464],{"class":303},"  request.",[283,1466,1467],{"class":299},"useRequest",[283,1469,334],{"class":289},[283,1471,378],{"class":303},[283,1473,1474],{"class":313},"fn",[283,1476,317],{"class":289},[283,1478,1048],{"class":299},[283,1480,1116],{"class":303},[283,1482,949],{"class":289},[283,1484,471],{"class":303},[283,1486,1488,1491,1494],{"class":285,"line":1487},21,[283,1489,1490],{"class":303},"    requestInterceptors.",[283,1492,1493],{"class":299},"push",[283,1495,1496],{"class":303},"(fn);\n",[283,1498,1500,1502,1504,1506,1508,1511,1514,1517,1520,1523,1526],{"class":285,"line":1499},22,[283,1501,706],{"class":289},[283,1503,946],{"class":303},[283,1505,949],{"class":289},[283,1507,1083],{"class":289},[283,1509,1510],{"class":303}," requestInterceptors.",[283,1512,1513],{"class":299},"splice",[283,1515,1516],{"class":303},"(requestInterceptors.",[283,1518,1519],{"class":299},"indexOf",[283,1521,1522],{"class":303},"(fn), ",[283,1524,1525],{"class":320},"1",[283,1527,788],{"class":303},[283,1529,1531],{"class":285,"line":1530},23,[283,1532,728],{"class":303},[283,1534,1536,1538,1541,1543,1545,1547,1549,1551,1553,1555],{"class":285,"line":1535},24,[283,1537,1464],{"class":303},[283,1539,1540],{"class":299},"useResponse",[283,1542,334],{"class":289},[283,1544,378],{"class":303},[283,1546,1474],{"class":313},[283,1548,317],{"class":289},[283,1550,1101],{"class":299},[283,1552,1116],{"class":303},[283,1554,949],{"class":289},[283,1556,471],{"class":303},[283,1558,1560,1563,1565],{"class":285,"line":1559},25,[283,1561,1562],{"class":303},"    responseInterceptors.",[283,1564,1493],{"class":299},[283,1566,1496],{"class":303},[283,1568,1570,1572,1574,1576,1578,1581,1583,1586,1588,1590,1592],{"class":285,"line":1569},26,[283,1571,706],{"class":289},[283,1573,946],{"class":303},[283,1575,949],{"class":289},[283,1577,1083],{"class":289},[283,1579,1580],{"class":303}," responseInterceptors.",[283,1582,1513],{"class":299},[283,1584,1585],{"class":303},"(responseInterceptors.",[283,1587,1519],{"class":299},[283,1589,1522],{"class":303},[283,1591,1525],{"class":320},[283,1593,788],{"class":303},[283,1595,1597],{"class":285,"line":1596},27,[283,1598,728],{"class":303},[283,1600,1602],{"class":285,"line":1601},28,[283,1603,543],{"emptyLinePlaceholder":542},[283,1605,1607,1609],{"class":285,"line":1606},29,[283,1608,414],{"class":289},[283,1610,1611],{"class":303}," request;\n",[283,1613,1615],{"class":285,"line":1614},30,[283,1616,440],{"class":303},[10,1618,1619,1620,1623],{},"Each ",[14,1621,1622],{},"use*"," method returns an unsubscribe function, which matters when you register an interceptor from a component and want to clean up on unmount.",[10,1625,1626],{},"Auth becomes a one-liner:",[274,1628,1630],{"className":276,"code":1629,"language":278,"meta":279,"style":279},"api.useRequest((ctx) => {\n  ctx.init.headers = { ...(ctx.init.headers as Record\u003Cstring, string>), Authorization: `Bearer ${token.value}` };\n});\n",[14,1631,1632,1650,1696],{"__ignoreMap":279},[283,1633,1634,1637,1639,1642,1644,1646,1648],{"class":285,"line":286},[283,1635,1636],{"class":303},"api.",[283,1638,1467],{"class":299},[283,1640,1641],{"class":303},"((",[283,1643,1055],{"class":313},[283,1645,1116],{"class":303},[283,1647,949],{"class":289},[283,1649,471],{"class":303},[283,1651,1652,1655,1657,1659,1661,1664,1666,1668,1670,1672,1674,1676,1679,1682,1685,1687,1690,1693],{"class":285,"line":352},[283,1653,1654],{"class":303},"  ctx.init.headers ",[283,1656,533],{"class":289},[283,1658,517],{"class":303},[283,1660,628],{"class":289},[283,1662,1663],{"class":303},"(ctx.init.headers ",[283,1665,425],{"class":289},[283,1667,494],{"class":299},[283,1669,304],{"class":303},[283,1671,499],{"class":320},[283,1673,180],{"class":303},[283,1675,499],{"class":320},[283,1677,1678],{"class":303},">), Authorization: ",[283,1680,1681],{"class":895},"`Bearer ${",[283,1683,1684],{"class":303},"token",[283,1686,60],{"class":895},[283,1688,1689],{"class":303},"value",[283,1691,1692],{"class":895},"}`",[283,1694,1695],{"class":303}," };\n",[283,1697,1698],{"class":285,"line":372},[283,1699,1700],{"class":303},"});\n",[10,1702,1703],{},"So does \"refresh on 401\":",[274,1705,1707],{"className":276,"code":1706,"language":278,"meta":279,"style":279},"api.useResponse(async (res) => {\n  if (res.status !== 401) return res;\n  await refreshAuth();\n  return fetch(res.url, { \u002F* re-run options *\u002F });\n});\n",[14,1708,1709,1730,1750,1760,1774],{"__ignoreMap":279},[283,1710,1711,1713,1715,1717,1720,1722,1724,1726,1728],{"class":285,"line":286},[283,1712,1636],{"class":303},[283,1714,1540],{"class":299},[283,1716,463],{"class":303},[283,1718,1719],{"class":289},"async",[283,1721,378],{"class":303},[283,1723,1108],{"class":313},[283,1725,1116],{"class":303},[283,1727,949],{"class":289},[283,1729,471],{"class":303},[283,1731,1732,1734,1737,1740,1743,1745,1747],{"class":285,"line":352},[283,1733,375],{"class":289},[283,1735,1736],{"class":303}," (res.status ",[283,1738,1739],{"class":289},"!==",[283,1741,1742],{"class":320}," 401",[283,1744,1116],{"class":303},[283,1746,867],{"class":289},[283,1748,1749],{"class":303}," res;\n",[283,1751,1752,1755,1758],{"class":285,"line":372},[283,1753,1754],{"class":289},"  await",[283,1756,1757],{"class":299}," refreshAuth",[283,1759,613],{"class":303},[283,1761,1762,1764,1766,1769,1772],{"class":285,"line":411},[283,1763,414],{"class":289},[283,1765,366],{"class":299},[283,1767,1768],{"class":303},"(res.url, { ",[283,1770,1771],{"class":917},"\u002F* re-run options *\u002F",[283,1773,979],{"class":303},[283,1775,1776],{"class":285,"line":437},[283,1777,1700],{"class":303},[266,1779,1781],{"id":1780},"layer-4-retry-with-exponential-backoff","Layer 4: retry with exponential backoff",[10,1783,1784],{},"For idempotent requests (GET, HEAD, most PUTs, DELETEs), a small retry helper absorbs transient network failures:",[274,1786,1788],{"className":276,"code":1787,"language":278,"meta":279,"style":279},"async function withRetry\u003CT>(fn: () => Promise\u003CT>, attempts = 2, baseDelayMs = 200): Promise\u003CT> {\n  let lastError: unknown;\n  for (let i = 0; i \u003C= attempts; i++) {\n    try {\n      return await fn();\n    } catch (e) {\n      lastError = e;\n      if (i \u003C attempts) await new Promise((r) => setTimeout(r, baseDelayMs * 2 ** i));\n    }\n  }\n  throw lastError;\n}\n",[14,1789,1790,1852,1867,1900,1907,1918,1929,1939,1984,1989,1993,2001],{"__ignoreMap":279},[283,1791,1792,1794,1796,1799,1801,1803,1805,1807,1809,1811,1813,1815,1817,1819,1822,1825,1827,1830,1832,1835,1837,1840,1842,1844,1846,1848,1850],{"class":285,"line":286},[283,1793,1719],{"class":289},[283,1795,296],{"class":289},[283,1797,1798],{"class":299}," withRetry",[283,1800,304],{"class":303},[283,1802,307],{"class":299},[283,1804,310],{"class":303},[283,1806,1474],{"class":299},[283,1808,317],{"class":289},[283,1810,946],{"class":303},[283,1812,949],{"class":289},[283,1814,342],{"class":299},[283,1816,304],{"class":303},[283,1818,307],{"class":299},[283,1820,1821],{"class":303},">, ",[283,1823,1824],{"class":313},"attempts",[283,1826,334],{"class":289},[283,1828,1829],{"class":320}," 2",[283,1831,180],{"class":303},[283,1833,1834],{"class":313},"baseDelayMs",[283,1836,334],{"class":289},[283,1838,1839],{"class":320}," 200",[283,1841,841],{"class":303},[283,1843,317],{"class":289},[283,1845,342],{"class":299},[283,1847,304],{"class":303},[283,1849,307],{"class":299},[283,1851,349],{"class":303},[283,1853,1854,1857,1860,1862,1865],{"class":285,"line":352},[283,1855,1856],{"class":289},"  let",[283,1858,1859],{"class":303}," lastError",[283,1861,317],{"class":289},[283,1863,1864],{"class":320}," unknown",[283,1866,483],{"class":303},[283,1868,1869,1872,1874,1877,1880,1882,1885,1888,1891,1894,1897],{"class":285,"line":372},[283,1870,1871],{"class":289},"  for",[283,1873,378],{"class":303},[283,1875,1876],{"class":289},"let",[283,1878,1879],{"class":303}," i ",[283,1881,533],{"class":289},[283,1883,1884],{"class":320}," 0",[283,1886,1887],{"class":303},"; i ",[283,1889,1890],{"class":289},"\u003C=",[283,1892,1893],{"class":303}," attempts; i",[283,1895,1896],{"class":289},"++",[283,1898,1899],{"class":303},") {\n",[283,1901,1902,1905],{"class":285,"line":411},[283,1903,1904],{"class":289},"    try",[283,1906,471],{"class":303},[283,1908,1909,1912,1914,1916],{"class":285,"line":437},[283,1910,1911],{"class":289},"      return",[283,1913,363],{"class":289},[283,1915,1344],{"class":299},[283,1917,613],{"class":303},[283,1919,1920,1923,1926],{"class":285,"line":539},[283,1921,1922],{"class":303},"    } ",[283,1924,1925],{"class":289},"catch",[283,1927,1928],{"class":303}," (e) {\n",[283,1930,1931,1934,1936],{"class":285,"line":546},[283,1932,1933],{"class":303},"      lastError ",[283,1935,533],{"class":289},[283,1937,1938],{"class":303}," e;\n",[283,1940,1941,1944,1947,1949,1952,1954,1956,1958,1960,1963,1965,1967,1970,1973,1976,1978,1981],{"class":285,"line":591},[283,1942,1943],{"class":289},"      if",[283,1945,1946],{"class":303}," (i ",[283,1948,304],{"class":289},[283,1950,1951],{"class":303}," attempts) ",[283,1953,399],{"class":289},[283,1955,390],{"class":289},[283,1957,342],{"class":320},[283,1959,1641],{"class":303},[283,1961,1962],{"class":313},"r",[283,1964,1116],{"class":303},[283,1966,949],{"class":289},[283,1968,1969],{"class":299}," setTimeout",[283,1971,1972],{"class":303},"(r, baseDelayMs ",[283,1974,1975],{"class":289},"*",[283,1977,1829],{"class":320},[283,1979,1980],{"class":289}," **",[283,1982,1983],{"class":303}," i));\n",[283,1985,1986],{"class":285,"line":616},[283,1987,1988],{"class":303},"    }\n",[283,1990,1991],{"class":285,"line":654},[283,1992,1453],{"class":303},[283,1994,1995,1998],{"class":285,"line":675},[283,1996,1997],{"class":289},"  throw",[283,1999,2000],{"class":303}," lastError;\n",[283,2002,2003],{"class":285,"line":703},[283,2004,440],{"class":303},[10,2006,2007,2008,2011],{},"Three attempts with 200ms \u002F 400ms \u002F 800ms backoff covers most spurious 502s and DNS blips. Retry non-idempotent requests at your peril: re-sending a POST that already committed is worse than failing loudly. A small allow-list (",[14,2009,2010],{},"['GET', 'HEAD', 'PUT', 'DELETE']",") inside the wrapper is usually how this gets enforced.",[266,2013,2015],{"id":2014},"what-you-end-up-with","What you end up with",[10,2017,2018],{},"After those four layers, the consumer surface looks like:",[274,2020,2022],{"className":276,"code":2021,"language":278,"meta":279,"style":279},"const api = createClient({\n  baseURL: 'https:\u002F\u002Fapi.example.com',\n  defaultHeaders: { Accept: 'application\u002Fjson' },\n});\n\napi.useRequest((ctx) => {\n  ctx.init.headers = { ...(ctx.init.headers as Record\u003Cstring, string>), Authorization: `Bearer ${token.value}` };\n});\n\nconst users = await api\u003CUser[]>('\u002Fusers');\n",[14,2023,2024,2038,2049,2060,2064,2068,2084,2122,2126,2130],{"__ignoreMap":279},[283,2025,2026,2028,2031,2033,2035],{"class":285,"line":286},[283,2027,769],{"class":289},[283,2029,2030],{"class":320}," api",[283,2032,334],{"class":289},[283,2034,460],{"class":299},[283,2036,2037],{"class":303},"({\n",[283,2039,2040,2043,2046],{"class":285,"line":352},[283,2041,2042],{"class":303},"  baseURL: ",[283,2044,2045],{"class":895},"'https:\u002F\u002Fapi.example.com'",[283,2047,2048],{"class":303},",\n",[283,2050,2051,2054,2057],{"class":285,"line":372},[283,2052,2053],{"class":303},"  defaultHeaders: { Accept: ",[283,2055,2056],{"class":895},"'application\u002Fjson'",[283,2058,2059],{"class":303}," },\n",[283,2061,2062],{"class":285,"line":411},[283,2063,1700],{"class":303},[283,2065,2066],{"class":285,"line":437},[283,2067,543],{"emptyLinePlaceholder":542},[283,2069,2070,2072,2074,2076,2078,2080,2082],{"class":285,"line":539},[283,2071,1636],{"class":303},[283,2073,1467],{"class":299},[283,2075,1641],{"class":303},[283,2077,1055],{"class":313},[283,2079,1116],{"class":303},[283,2081,949],{"class":289},[283,2083,471],{"class":303},[283,2085,2086,2088,2090,2092,2094,2096,2098,2100,2102,2104,2106,2108,2110,2112,2114,2116,2118,2120],{"class":285,"line":546},[283,2087,1654],{"class":303},[283,2089,533],{"class":289},[283,2091,517],{"class":303},[283,2093,628],{"class":289},[283,2095,1663],{"class":303},[283,2097,425],{"class":289},[283,2099,494],{"class":299},[283,2101,304],{"class":303},[283,2103,499],{"class":320},[283,2105,180],{"class":303},[283,2107,499],{"class":320},[283,2109,1678],{"class":303},[283,2111,1681],{"class":895},[283,2113,1684],{"class":303},[283,2115,60],{"class":895},[283,2117,1689],{"class":303},[283,2119,1692],{"class":895},[283,2121,1695],{"class":303},[283,2123,2124],{"class":285,"line":591},[283,2125,1700],{"class":303},[283,2127,2128],{"class":285,"line":616},[283,2129,543],{"emptyLinePlaceholder":542},[283,2131,2132,2134,2137,2139,2141,2143,2145,2148,2151,2154],{"class":285,"line":654},[283,2133,769],{"class":289},[283,2135,2136],{"class":320}," users",[283,2138,334],{"class":289},[283,2140,363],{"class":289},[283,2142,2030],{"class":299},[283,2144,304],{"class":303},[283,2146,2147],{"class":299},"User",[283,2149,2150],{"class":303},"[]>(",[283,2152,2153],{"class":895},"'\u002Fusers'",[283,2155,788],{"class":303},[10,2157,2158,2159,60],{},"Flat call-shape, auth handled once, errors thrown as a known type, timeouts and retry applied where appropriate. Every layer is a plain function wrapping the one below. At the bottom is still ",[14,2160,16],{},[10,2162,2163],{},"Which is exactly where the upload-progress gap lives.",[94,2165,2167,2168,2170],{"id":2166},"why-fetch-cant-show-upload-progress","Why ",[14,2169,16],{}," can't show upload progress",[10,2172,2173,2174,2176],{},"Download progress with ",[14,2175,16],{}," works, but awkwardly. You read the response body as a stream:",[274,2178,2180],{"className":276,"code":2179,"language":278,"meta":279,"style":279},"const res = await fetch('\u002Fbig.zip');\nconst total = Number(res.headers.get('content-length')) || null;\nlet loaded = 0;\nconst reader = res.body!.getReader();\nwhile (true) {\n  const { value, done } = await reader.read();\n  if (done) break;\n  loaded += value.byteLength;\n  onProgress(loaded, total);\n}\n",[14,2181,2182,2201,2235,2248,2269,2280,2308,2320,2331,2339],{"__ignoreMap":279},[283,2183,2184,2186,2188,2190,2192,2194,2196,2199],{"class":285,"line":286},[283,2185,769],{"class":289},[283,2187,358],{"class":320},[283,2189,334],{"class":289},[283,2191,363],{"class":289},[283,2193,366],{"class":299},[283,2195,463],{"class":303},[283,2197,2198],{"class":895},"'\u002Fbig.zip'",[283,2200,788],{"class":303},[283,2202,2203,2205,2208,2210,2213,2216,2219,2221,2224,2227,2230,2233],{"class":285,"line":352},[283,2204,769],{"class":289},[283,2206,2207],{"class":320}," total",[283,2209,334],{"class":289},[283,2211,2212],{"class":299}," Number",[283,2214,2215],{"class":303},"(res.headers.",[283,2217,2218],{"class":299},"get",[283,2220,463],{"class":303},[283,2222,2223],{"class":895},"'content-length'",[283,2225,2226],{"class":303},")) ",[283,2228,2229],{"class":289},"||",[283,2231,2232],{"class":320}," null",[283,2234,483],{"class":303},[283,2236,2237,2239,2242,2244,2246],{"class":285,"line":372},[283,2238,1876],{"class":289},[283,2240,2241],{"class":303}," loaded ",[283,2243,533],{"class":289},[283,2245,1884],{"class":320},[283,2247,483],{"class":303},[283,2249,2250,2252,2255,2257,2260,2262,2264,2267],{"class":285,"line":411},[283,2251,769],{"class":289},[283,2253,2254],{"class":320}," reader",[283,2256,334],{"class":289},[283,2258,2259],{"class":303}," res.body",[283,2261,381],{"class":289},[283,2263,60],{"class":303},[283,2265,2266],{"class":299},"getReader",[283,2268,613],{"class":303},[283,2270,2271,2274,2276,2278],{"class":285,"line":437},[283,2272,2273],{"class":289},"while",[283,2275,378],{"class":303},[283,2277,976],{"class":320},[283,2279,1899],{"class":303},[283,2281,2282,2284,2286,2288,2290,2293,2296,2298,2300,2303,2306],{"class":285,"line":539},[283,2283,355],{"class":289},[283,2285,517],{"class":303},[283,2287,1689],{"class":320},[283,2289,180],{"class":303},[283,2291,2292],{"class":320},"done",[283,2294,2295],{"class":303}," } ",[283,2297,533],{"class":289},[283,2299,363],{"class":289},[283,2301,2302],{"class":303}," reader.",[283,2304,2305],{"class":299},"read",[283,2307,613],{"class":303},[283,2309,2310,2312,2315,2318],{"class":285,"line":546},[283,2311,375],{"class":289},[283,2313,2314],{"class":303}," (done) ",[283,2316,2317],{"class":289},"break",[283,2319,483],{"class":303},[283,2321,2322,2325,2328],{"class":285,"line":591},[283,2323,2324],{"class":303},"  loaded ",[283,2326,2327],{"class":289},"+=",[283,2329,2330],{"class":303}," value.byteLength;\n",[283,2332,2333,2336],{"class":285,"line":616},[283,2334,2335],{"class":299},"  onProgress",[283,2337,2338],{"class":303},"(loaded, total);\n",[283,2340,2341],{"class":285,"line":654},[283,2342,440],{"class":303},[10,2344,2345,2346,2348],{},"That's fine. You do have to keep the chunks around yourself (or pipe them into a new ",[14,2347,28],{}," if you want to hand them off), but the primitives are there.",[10,2350,2351,2352,2354,2355,2357,2358,2361,2362,2364,2365,2368],{},"For uploads, the missing piece is that the ",[14,2353,24],{}," body is consumed by the browser's network stack, not by your code. You can pass a ",[14,2356,44],{},", but there's no event that fires as the browser drains it. Some Chromium versions support ",[14,2359,2360],{},"Request.duplex: 'half'"," which opens the door to upload streaming, but there's still no ",[14,2363,48],{}," event. You'd have to instrument your own stream's ",[14,2366,2367],{},"pull()"," to count bytes, and even then you're measuring what the stream emits, not what the socket has sent.",[10,2370,2371],{},"In practice, if you want a progress bar for uploads in 2026, you use XHR.",[94,2373,2375],{"id":2374},"the-five-xhr-events-you-actually-care-about","The five XHR events you actually care about",[10,2377,2378],{},"Every XHR fires a predictable cluster of events. The ones that matter for progress UIs are:",[274,2380,2382],{"className":276,"code":2381,"language":278,"meta":279,"style":279},"xhr.upload.onprogress = (e: ProgressEvent) => { \u002F* upload bytes *\u002F };\nxhr.onprogress        = (e: ProgressEvent) => { \u002F* download bytes *\u002F };\nxhr.onload            = () => { \u002F* request completed successfully *\u002F };\nxhr.onerror           = () => { \u002F* network error *\u002F };\nxhr.onabort           = () => { \u002F* xhr.abort() was called *\u002F };\nxhr.ontimeout         = () => { \u002F* xhr.timeout exceeded *\u002F };\n",[14,2383,2384,2415,2444,2465,2486,2506],{"__ignoreMap":279},[283,2385,2386,2389,2392,2394,2396,2399,2401,2404,2406,2408,2410,2413],{"class":285,"line":286},[283,2387,2388],{"class":303},"xhr.upload.",[283,2390,2391],{"class":299},"onprogress",[283,2393,334],{"class":289},[283,2395,378],{"class":303},[283,2397,2398],{"class":313},"e",[283,2400,317],{"class":289},[283,2402,2403],{"class":299}," ProgressEvent",[283,2405,1116],{"class":303},[283,2407,949],{"class":289},[283,2409,517],{"class":303},[283,2411,2412],{"class":917},"\u002F* upload bytes *\u002F",[283,2414,1695],{"class":303},[283,2416,2417,2420,2422,2425,2427,2429,2431,2433,2435,2437,2439,2442],{"class":285,"line":352},[283,2418,2419],{"class":303},"xhr.",[283,2421,2391],{"class":299},[283,2423,2424],{"class":289},"        =",[283,2426,378],{"class":303},[283,2428,2398],{"class":313},[283,2430,317],{"class":289},[283,2432,2403],{"class":299},[283,2434,1116],{"class":303},[283,2436,949],{"class":289},[283,2438,517],{"class":303},[283,2440,2441],{"class":917},"\u002F* download bytes *\u002F",[283,2443,1695],{"class":303},[283,2445,2446,2448,2451,2454,2456,2458,2460,2463],{"class":285,"line":372},[283,2447,2419],{"class":303},[283,2449,2450],{"class":299},"onload",[283,2452,2453],{"class":289},"            =",[283,2455,946],{"class":303},[283,2457,949],{"class":289},[283,2459,517],{"class":303},[283,2461,2462],{"class":917},"\u002F* request completed successfully *\u002F",[283,2464,1695],{"class":303},[283,2466,2467,2469,2472,2475,2477,2479,2481,2484],{"class":285,"line":411},[283,2468,2419],{"class":303},[283,2470,2471],{"class":299},"onerror",[283,2473,2474],{"class":289},"           =",[283,2476,946],{"class":303},[283,2478,949],{"class":289},[283,2480,517],{"class":303},[283,2482,2483],{"class":917},"\u002F* network error *\u002F",[283,2485,1695],{"class":303},[283,2487,2488,2490,2493,2495,2497,2499,2501,2504],{"class":285,"line":437},[283,2489,2419],{"class":303},[283,2491,2492],{"class":299},"onabort",[283,2494,2474],{"class":289},[283,2496,946],{"class":303},[283,2498,949],{"class":289},[283,2500,517],{"class":303},[283,2502,2503],{"class":917},"\u002F* xhr.abort() was called *\u002F",[283,2505,1695],{"class":303},[283,2507,2508,2510,2513,2516,2518,2520,2522,2525],{"class":285,"line":539},[283,2509,2419],{"class":303},[283,2511,2512],{"class":299},"ontimeout",[283,2514,2515],{"class":289},"         =",[283,2517,946],{"class":303},[283,2519,949],{"class":289},[283,2521,517],{"class":303},[283,2523,2524],{"class":917},"\u002F* xhr.timeout exceeded *\u002F",[283,2526,1695],{"class":303},[10,2528,2529],{},"Two things are easy to miss.",[10,2531,2532,2533,2536,2537,2540,2541,2543,2544,2547,2548,2550],{},"First, ",[14,2534,2535],{},"xhr.upload"," is a separate ",[14,2538,2539],{},"XMLHttpRequestUpload"," object with its own event target. It fires ",[14,2542,48],{}," as the request body is sent; ",[14,2545,2546],{},"xhr"," itself fires ",[14,2549,48],{}," as the response body is received. They're the two phases of the same request, and they use the same event shape.",[10,2552,2553,2554,2557,2558,2561,2562,60],{},"Second, ",[14,2555,2556],{},"xhr.onload"," fires on any completed request, including one that returned HTTP 500. \"Request completed\" here means \"the server replied.\" If you want \"the request succeeded,\" check ",[14,2559,2560],{},"xhr.status"," yourself inside ",[14,2563,2450],{},[10,2565,2566,2567,180,2570,180,2573,2576],{},"There are other events (",[14,2568,2569],{},"loadstart",[14,2571,2572],{},"loadend",[14,2574,2575],{},"readystatechange",") but for a progress UI the five above cover everything you need.",[94,2578,2580,2581,2584,2585],{"id":2579},"the-progressevent-shape-and-lengthcomputable","The ",[14,2582,2583],{},"ProgressEvent"," shape, and ",[14,2586,2587],{},"lengthComputable",[10,2589,2590,2591,2593,2594,2596],{},"Both ",[14,2592,2391],{}," callbacks receive a ",[14,2595,2583],{}," with three fields:",[274,2598,2600],{"className":276,"code":2599,"language":278,"meta":279,"style":279},"interface ProgressEvent {\n  lengthComputable: boolean;\n  loaded: number;   \u002F\u002F bytes transferred so far\n  total: number;    \u002F\u002F bytes expected, ONLY valid when lengthComputable is true\n}\n",[14,2601,2602,2611,2623,2639,2654],{"__ignoreMap":279},[283,2603,2604,2607,2609],{"class":285,"line":286},[283,2605,2606],{"class":289},"interface",[283,2608,2403],{"class":299},[283,2610,471],{"class":303},[283,2612,2613,2616,2618,2621],{"class":285,"line":352},[283,2614,2615],{"class":313},"  lengthComputable",[283,2617,317],{"class":289},[283,2619,2620],{"class":320}," boolean",[283,2622,483],{"class":303},[283,2624,2625,2628,2630,2633,2636],{"class":285,"line":372},[283,2626,2627],{"class":313},"  loaded",[283,2629,317],{"class":289},[283,2631,2632],{"class":320}," number",[283,2634,2635],{"class":303},";   ",[283,2637,2638],{"class":917},"\u002F\u002F bytes transferred so far\n",[283,2640,2641,2644,2646,2648,2651],{"class":285,"line":411},[283,2642,2643],{"class":313},"  total",[283,2645,317],{"class":289},[283,2647,2632],{"class":320},[283,2649,2650],{"class":303},";    ",[283,2652,2653],{"class":917},"\u002F\u002F bytes expected, ONLY valid when lengthComputable is true\n",[283,2655,2656],{"class":285,"line":437},[283,2657,440],{"class":303},[10,2659,2660,2662,2663,2666,2667,2670,2671,2673,2674,2677,2678,2681,2682,2685,2686,254,2689,2692],{},[14,2661,2587],{}," is the field most first-time implementations forget. It's ",[14,2664,2665],{},"false"," when the server sent a chunked response without a ",[14,2668,2669],{},"Content-Length"," header, and it's ",[14,2672,2665],{}," during uploads where the body is a stream of unknown length. When it's false, ",[14,2675,2676],{},"total"," is ",[14,2679,2680],{},"0",": not \"not provided,\" not ",[14,2683,2684],{},"undefined",", just the number zero, and dividing by it gives you ",[14,2687,2688],{},"NaN",[14,2690,2691],{},"Infinity"," in your progress bar.",[10,2694,2695],{},"Wrap the normalization in one place:",[274,2697,2699],{"className":276,"code":2698,"language":278,"meta":279,"style":279},"function normaliseProgress(phase: 'upload' | 'download', e: ProgressEvent) {\n  const total = e.lengthComputable ? e.total : null;\n  const ratio = total && total > 0 ? e.loaded \u002F total : null;\n  return { phase, loaded: e.loaded, total, ratio };\n}\n",[14,2700,2701,2733,2756,2794,2801],{"__ignoreMap":279},[283,2702,2703,2705,2708,2710,2713,2715,2718,2720,2723,2725,2727,2729,2731],{"class":285,"line":286},[283,2704,816],{"class":289},[283,2706,2707],{"class":299}," normaliseProgress",[283,2709,463],{"class":303},[283,2711,2712],{"class":313},"phase",[283,2714,317],{"class":289},[283,2716,2717],{"class":895}," 'upload'",[283,2719,848],{"class":289},[283,2721,2722],{"class":895}," 'download'",[283,2724,180],{"class":303},[283,2726,2398],{"class":313},[283,2728,317],{"class":289},[283,2730,2403],{"class":299},[283,2732,1899],{"class":303},[283,2734,2735,2737,2739,2741,2744,2747,2750,2752,2754],{"class":285,"line":352},[283,2736,355],{"class":289},[283,2738,2207],{"class":320},[283,2740,334],{"class":289},[283,2742,2743],{"class":303}," e.lengthComputable ",[283,2745,2746],{"class":289},"?",[283,2748,2749],{"class":303}," e.total ",[283,2751,317],{"class":289},[283,2753,2232],{"class":320},[283,2755,483],{"class":303},[283,2757,2758,2760,2763,2765,2768,2771,2773,2776,2778,2781,2784,2786,2788,2790,2792],{"class":285,"line":372},[283,2759,355],{"class":289},[283,2761,2762],{"class":320}," ratio",[283,2764,334],{"class":289},[283,2766,2767],{"class":303}," total ",[283,2769,2770],{"class":289},"&&",[283,2772,2767],{"class":303},[283,2774,2775],{"class":289},">",[283,2777,1884],{"class":320},[283,2779,2780],{"class":289}," ?",[283,2782,2783],{"class":303}," e.loaded ",[283,2785,25],{"class":289},[283,2787,2767],{"class":303},[283,2789,317],{"class":289},[283,2791,2232],{"class":320},[283,2793,483],{"class":303},[283,2795,2796,2798],{"class":285,"line":411},[283,2797,414],{"class":289},[283,2799,2800],{"class":303}," { phase, loaded: e.loaded, total, ratio };\n",[283,2802,2803],{"class":285,"line":437},[283,2804,440],{"class":303},[10,2806,2807,2810,2811,2813],{},[14,2808,2809],{},"null"," is the honest answer when you don't know the total. The UI layer decides what to show: bytes counter with no percentage, an indeterminate bar, a spinner. Don't let ",[14,2812,2688],{}," leak out of your transport.",[94,2815,2817],{"id":2816},"a-minimal-upload-progress-example","A minimal upload-progress example",[10,2819,2820],{},"Here's the smallest useful upload with progress, showing the API surface without a wrapper:",[274,2822,2824],{"className":276,"code":2823,"language":278,"meta":279,"style":279},"async function uploadWithProgress(\n  file: File,\n  onProgress: (loaded: number, total: number | null) => void\n): Promise\u003Cvoid> {\n  await new Promise\u003Cvoid>((resolve, reject) => {\n    const xhr = new XMLHttpRequest();\n    xhr.open('POST', '\u002Fupload', true);\n\n    xhr.upload.onprogress = (e) => {\n      const total = e.lengthComputable ? e.total : null;\n      onProgress(e.loaded, total);\n    };\n\n    xhr.onload = () => {\n      if (xhr.status >= 200 && xhr.status \u003C 300) resolve();\n      else reject(new Error(`HTTP ${xhr.status}`));\n    };\n    xhr.onerror = () => reject(new TypeError('Network error'));\n    xhr.onabort = () => reject(new DOMException('Aborted', 'AbortError'));\n\n    const form = new FormData();\n    form.append('file', file);\n    xhr.send(form);\n  });\n}\n",[14,2825,2826,2838,2850,2884,2898,2927,2943,2967,2971,2990,3011,3019,3023,3027,3041,3070,3102,3106,3134,3167,3171,3187,3203,3213,3218],{"__ignoreMap":279},[283,2827,2828,2830,2832,2835],{"class":285,"line":286},[283,2829,1719],{"class":289},[283,2831,296],{"class":289},[283,2833,2834],{"class":299}," uploadWithProgress",[283,2836,2837],{"class":303},"(\n",[283,2839,2840,2843,2845,2848],{"class":285,"line":352},[283,2841,2842],{"class":313},"  file",[283,2844,317],{"class":289},[283,2846,2847],{"class":299}," File",[283,2849,2048],{"class":303},[283,2851,2852,2854,2856,2858,2861,2863,2865,2867,2869,2871,2873,2875,2877,2879,2881],{"class":285,"line":372},[283,2853,2335],{"class":299},[283,2855,317],{"class":289},[283,2857,378],{"class":303},[283,2859,2860],{"class":313},"loaded",[283,2862,317],{"class":289},[283,2864,2632],{"class":320},[283,2866,180],{"class":303},[283,2868,2676],{"class":313},[283,2870,317],{"class":289},[283,2872,2632],{"class":320},[283,2874,848],{"class":289},[283,2876,2232],{"class":320},[283,2878,1116],{"class":303},[283,2880,949],{"class":289},[283,2882,2883],{"class":320}," void\n",[283,2885,2886,2888,2890,2892,2894,2896],{"class":285,"line":411},[283,2887,841],{"class":303},[283,2889,317],{"class":289},[283,2891,342],{"class":299},[283,2893,304],{"class":303},[283,2895,1092],{"class":320},[283,2897,349],{"class":303},[283,2899,2900,2902,2904,2906,2908,2910,2913,2916,2918,2921,2923,2925],{"class":285,"line":437},[283,2901,1754],{"class":289},[283,2903,390],{"class":289},[283,2905,342],{"class":320},[283,2907,304],{"class":303},[283,2909,1092],{"class":320},[283,2911,2912],{"class":303},">((",[283,2914,2915],{"class":313},"resolve",[283,2917,180],{"class":303},[283,2919,2920],{"class":313},"reject",[283,2922,1116],{"class":303},[283,2924,949],{"class":289},[283,2926,471],{"class":303},[283,2928,2929,2931,2934,2936,2938,2941],{"class":285,"line":539},[283,2930,594],{"class":289},[283,2932,2933],{"class":320}," xhr",[283,2935,334],{"class":289},[283,2937,390],{"class":289},[283,2939,2940],{"class":299}," XMLHttpRequest",[283,2942,613],{"class":303},[283,2944,2945,2948,2951,2953,2956,2958,2961,2963,2965],{"class":285,"line":546},[283,2946,2947],{"class":303},"    xhr.",[283,2949,2950],{"class":299},"open",[283,2952,463],{"class":303},[283,2954,2955],{"class":895},"'POST'",[283,2957,180],{"class":303},[283,2959,2960],{"class":895},"'\u002Fupload'",[283,2962,180],{"class":303},[283,2964,976],{"class":320},[283,2966,788],{"class":303},[283,2968,2969],{"class":285,"line":591},[283,2970,543],{"emptyLinePlaceholder":542},[283,2972,2973,2976,2978,2980,2982,2984,2986,2988],{"class":285,"line":616},[283,2974,2975],{"class":303},"    xhr.upload.",[283,2977,2391],{"class":299},[283,2979,334],{"class":289},[283,2981,378],{"class":303},[283,2983,2398],{"class":313},[283,2985,1116],{"class":303},[283,2987,949],{"class":289},[283,2989,471],{"class":303},[283,2991,2992,2995,2997,2999,3001,3003,3005,3007,3009],{"class":285,"line":654},[283,2993,2994],{"class":289},"      const",[283,2996,2207],{"class":320},[283,2998,334],{"class":289},[283,3000,2743],{"class":303},[283,3002,2746],{"class":289},[283,3004,2749],{"class":303},[283,3006,317],{"class":289},[283,3008,2232],{"class":320},[283,3010,483],{"class":303},[283,3012,3013,3016],{"class":285,"line":675},[283,3014,3015],{"class":299},"      onProgress",[283,3017,3018],{"class":303},"(e.loaded, total);\n",[283,3020,3021],{"class":285,"line":703},[283,3022,1332],{"class":303},[283,3024,3025],{"class":285,"line":725},[283,3026,543],{"emptyLinePlaceholder":542},[283,3028,3029,3031,3033,3035,3037,3039],{"class":285,"line":731},[283,3030,2947],{"class":303},[283,3032,2450],{"class":299},[283,3034,334],{"class":289},[283,3036,946],{"class":303},[283,3038,949],{"class":289},[283,3040,471],{"class":303},[283,3042,3043,3045,3048,3051,3053,3056,3059,3061,3064,3066,3068],{"class":285,"line":1377},[283,3044,1943],{"class":289},[283,3046,3047],{"class":303}," (xhr.status ",[283,3049,3050],{"class":289},">=",[283,3052,1839],{"class":320},[283,3054,3055],{"class":289}," &&",[283,3057,3058],{"class":303}," xhr.status ",[283,3060,304],{"class":289},[283,3062,3063],{"class":320}," 300",[283,3065,1116],{"class":303},[283,3067,2915],{"class":299},[283,3069,613],{"class":303},[283,3071,3072,3075,3078,3080,3082,3085,3087,3090,3092,3094,3097,3099],{"class":285,"line":1402},[283,3073,3074],{"class":289},"      else",[283,3076,3077],{"class":299}," reject",[283,3079,463],{"class":303},[283,3081,1277],{"class":289},[283,3083,3084],{"class":299}," Error",[283,3086,463],{"class":303},[283,3088,3089],{"class":895},"`HTTP ${",[283,3091,2546],{"class":303},[283,3093,60],{"class":895},[283,3095,3096],{"class":303},"status",[283,3098,1692],{"class":895},[283,3100,3101],{"class":303},"));\n",[283,3103,3104],{"class":285,"line":1429},[283,3105,1332],{"class":303},[283,3107,3108,3110,3112,3114,3116,3118,3120,3122,3124,3127,3129,3132],{"class":285,"line":1450},[283,3109,2947],{"class":303},[283,3111,2471],{"class":299},[283,3113,334],{"class":289},[283,3115,946],{"class":303},[283,3117,949],{"class":289},[283,3119,3077],{"class":299},[283,3121,463],{"class":303},[283,3123,1277],{"class":289},[283,3125,3126],{"class":299}," TypeError",[283,3128,463],{"class":303},[283,3130,3131],{"class":895},"'Network error'",[283,3133,3101],{"class":303},[283,3135,3136,3138,3140,3142,3144,3146,3148,3150,3152,3155,3157,3160,3162,3165],{"class":285,"line":1456},[283,3137,2947],{"class":303},[283,3139,2492],{"class":299},[283,3141,334],{"class":289},[283,3143,946],{"class":303},[283,3145,949],{"class":289},[283,3147,3077],{"class":299},[283,3149,463],{"class":303},[283,3151,1277],{"class":289},[283,3153,3154],{"class":299}," DOMException",[283,3156,463],{"class":303},[283,3158,3159],{"class":895},"'Aborted'",[283,3161,180],{"class":303},[283,3163,3164],{"class":895},"'AbortError'",[283,3166,3101],{"class":303},[283,3168,3169],{"class":285,"line":1461},[283,3170,543],{"emptyLinePlaceholder":542},[283,3172,3173,3175,3178,3180,3182,3185],{"class":285,"line":1487},[283,3174,594],{"class":289},[283,3176,3177],{"class":320}," form",[283,3179,334],{"class":289},[283,3181,390],{"class":289},[283,3183,3184],{"class":299}," FormData",[283,3186,613],{"class":303},[283,3188,3189,3192,3195,3197,3200],{"class":285,"line":1499},[283,3190,3191],{"class":303},"    form.",[283,3193,3194],{"class":299},"append",[283,3196,463],{"class":303},[283,3198,3199],{"class":895},"'file'",[283,3201,3202],{"class":303},", file);\n",[283,3204,3205,3207,3210],{"class":285,"line":1530},[283,3206,2947],{"class":303},[283,3208,3209],{"class":299},"send",[283,3211,3212],{"class":303},"(form);\n",[283,3214,3215],{"class":285,"line":1535},[283,3216,3217],{"class":303},"  });\n",[283,3219,3220],{"class":285,"line":1559},[283,3221,440],{"class":303},[10,3223,3224],{},"Three things worth pointing out:",[3226,3227,3228,3245,3255],"ul",{},[68,3229,3230,3240,3241,3244],{},[106,3231,3232,3233,3236,3237,60],{},"Don't set ",[14,3234,3235],{},"Content-Type"," when sending ",[14,3238,3239],{},"FormData"," The browser needs to pick its own boundary string for multipart encoding. If you set ",[14,3242,3243],{},"Content-Type: multipart\u002Fform-data"," manually, you'll override it without the boundary, and the server will fail to parse the body.",[68,3246,3247,109,3252,3254],{},[106,3248,3249,3251],{},[14,3250,2560],{}," check is explicit.",[14,3253,2450],{}," fires for 500s. Treating \"the request completed\" and \"the request succeeded\" as the same thing is the single most common source of silent upload bugs.",[68,3256,3257,3263,3264,3267],{},[106,3258,3259,3262],{},[14,3260,3261],{},"new Promise"," is the adapter."," XHR is event-based; the rest of your code is ",[14,3265,3266],{},"async\u002Fawait","-shaped. Wrapping it once in a Promise keeps the awkwardness local.",[94,3269,3271],{"id":3270},"a-minimal-download-progress-example","A minimal download-progress example",[10,3273,3274,3275,317],{},"For downloads, the same shape, but on ",[14,3276,3277],{},"xhr.onprogress",[274,3279,3281],{"className":276,"code":3280,"language":278,"meta":279,"style":279},"async function downloadWithProgress(\n  url: string,\n  onProgress: (loaded: number, total: number | null) => void\n): Promise\u003CBlob> {\n  return new Promise\u003CBlob>((resolve, reject) => {\n    const xhr = new XMLHttpRequest();\n    xhr.open('GET', url, true);\n    xhr.responseType = 'blob';\n\n    xhr.onprogress = (e) => {\n      const total = e.lengthComputable ? e.total : null;\n      onProgress(e.loaded, total);\n    };\n\n    xhr.onload = () => {\n      if (xhr.status >= 200 && xhr.status \u003C 300) {\n        resolve(xhr.response as Blob);\n      } else {\n        reject(new Error(`HTTP ${xhr.status}`));\n      }\n    };\n    xhr.onerror = () => reject(new TypeError('Network error'));\n\n    xhr.send();\n  });\n}\n",[14,3282,3283,3294,3305,3337,3352,3378,3392,3410,3422,3426,3444,3464,3470,3474,3478,3492,3512,3527,3537,3562,3567,3571,3597,3601,3609,3613],{"__ignoreMap":279},[283,3284,3285,3287,3289,3292],{"class":285,"line":286},[283,3286,1719],{"class":289},[283,3288,296],{"class":289},[283,3290,3291],{"class":299}," downloadWithProgress",[283,3293,2837],{"class":303},[283,3295,3296,3299,3301,3303],{"class":285,"line":352},[283,3297,3298],{"class":313},"  url",[283,3300,317],{"class":289},[283,3302,321],{"class":320},[283,3304,2048],{"class":303},[283,3306,3307,3309,3311,3313,3315,3317,3319,3321,3323,3325,3327,3329,3331,3333,3335],{"class":285,"line":372},[283,3308,2335],{"class":299},[283,3310,317],{"class":289},[283,3312,378],{"class":303},[283,3314,2860],{"class":313},[283,3316,317],{"class":289},[283,3318,2632],{"class":320},[283,3320,180],{"class":303},[283,3322,2676],{"class":313},[283,3324,317],{"class":289},[283,3326,2632],{"class":320},[283,3328,848],{"class":289},[283,3330,2232],{"class":320},[283,3332,1116],{"class":303},[283,3334,949],{"class":289},[283,3336,2883],{"class":320},[283,3338,3339,3341,3343,3345,3347,3350],{"class":285,"line":411},[283,3340,841],{"class":303},[283,3342,317],{"class":289},[283,3344,342],{"class":299},[283,3346,304],{"class":303},[283,3348,3349],{"class":299},"Blob",[283,3351,349],{"class":303},[283,3353,3354,3356,3358,3360,3362,3364,3366,3368,3370,3372,3374,3376],{"class":285,"line":437},[283,3355,414],{"class":289},[283,3357,390],{"class":289},[283,3359,342],{"class":320},[283,3361,304],{"class":303},[283,3363,3349],{"class":299},[283,3365,2912],{"class":303},[283,3367,2915],{"class":313},[283,3369,180],{"class":303},[283,3371,2920],{"class":313},[283,3373,1116],{"class":303},[283,3375,949],{"class":289},[283,3377,471],{"class":303},[283,3379,3380,3382,3384,3386,3388,3390],{"class":285,"line":539},[283,3381,594],{"class":289},[283,3383,2933],{"class":320},[283,3385,334],{"class":289},[283,3387,390],{"class":289},[283,3389,2940],{"class":299},[283,3391,613],{"class":303},[283,3393,3394,3396,3398,3400,3403,3406,3408],{"class":285,"line":546},[283,3395,2947],{"class":303},[283,3397,2950],{"class":299},[283,3399,463],{"class":303},[283,3401,3402],{"class":895},"'GET'",[283,3404,3405],{"class":303},", url, ",[283,3407,976],{"class":320},[283,3409,788],{"class":303},[283,3411,3412,3415,3417,3420],{"class":285,"line":591},[283,3413,3414],{"class":303},"    xhr.responseType ",[283,3416,533],{"class":289},[283,3418,3419],{"class":895}," 'blob'",[283,3421,483],{"class":303},[283,3423,3424],{"class":285,"line":616},[283,3425,543],{"emptyLinePlaceholder":542},[283,3427,3428,3430,3432,3434,3436,3438,3440,3442],{"class":285,"line":654},[283,3429,2947],{"class":303},[283,3431,2391],{"class":299},[283,3433,334],{"class":289},[283,3435,378],{"class":303},[283,3437,2398],{"class":313},[283,3439,1116],{"class":303},[283,3441,949],{"class":289},[283,3443,471],{"class":303},[283,3445,3446,3448,3450,3452,3454,3456,3458,3460,3462],{"class":285,"line":675},[283,3447,2994],{"class":289},[283,3449,2207],{"class":320},[283,3451,334],{"class":289},[283,3453,2743],{"class":303},[283,3455,2746],{"class":289},[283,3457,2749],{"class":303},[283,3459,317],{"class":289},[283,3461,2232],{"class":320},[283,3463,483],{"class":303},[283,3465,3466,3468],{"class":285,"line":703},[283,3467,3015],{"class":299},[283,3469,3018],{"class":303},[283,3471,3472],{"class":285,"line":725},[283,3473,1332],{"class":303},[283,3475,3476],{"class":285,"line":731},[283,3477,543],{"emptyLinePlaceholder":542},[283,3479,3480,3482,3484,3486,3488,3490],{"class":285,"line":1377},[283,3481,2947],{"class":303},[283,3483,2450],{"class":299},[283,3485,334],{"class":289},[283,3487,946],{"class":303},[283,3489,949],{"class":289},[283,3491,471],{"class":303},[283,3493,3494,3496,3498,3500,3502,3504,3506,3508,3510],{"class":285,"line":1402},[283,3495,1943],{"class":289},[283,3497,3047],{"class":303},[283,3499,3050],{"class":289},[283,3501,1839],{"class":320},[283,3503,3055],{"class":289},[283,3505,3058],{"class":303},[283,3507,304],{"class":289},[283,3509,3063],{"class":320},[283,3511,1899],{"class":303},[283,3513,3514,3517,3520,3522,3525],{"class":285,"line":1429},[283,3515,3516],{"class":299},"        resolve",[283,3518,3519],{"class":303},"(xhr.response ",[283,3521,425],{"class":289},[283,3523,3524],{"class":299}," Blob",[283,3526,788],{"class":303},[283,3528,3529,3532,3535],{"class":285,"line":1450},[283,3530,3531],{"class":303},"      } ",[283,3533,3534],{"class":289},"else",[283,3536,471],{"class":303},[283,3538,3539,3542,3544,3546,3548,3550,3552,3554,3556,3558,3560],{"class":285,"line":1456},[283,3540,3541],{"class":299},"        reject",[283,3543,463],{"class":303},[283,3545,1277],{"class":289},[283,3547,3084],{"class":299},[283,3549,463],{"class":303},[283,3551,3089],{"class":895},[283,3553,2546],{"class":303},[283,3555,60],{"class":895},[283,3557,3096],{"class":303},[283,3559,1692],{"class":895},[283,3561,3101],{"class":303},[283,3563,3564],{"class":285,"line":1461},[283,3565,3566],{"class":303},"      }\n",[283,3568,3569],{"class":285,"line":1487},[283,3570,1332],{"class":303},[283,3572,3573,3575,3577,3579,3581,3583,3585,3587,3589,3591,3593,3595],{"class":285,"line":1499},[283,3574,2947],{"class":303},[283,3576,2471],{"class":299},[283,3578,334],{"class":289},[283,3580,946],{"class":303},[283,3582,949],{"class":289},[283,3584,3077],{"class":299},[283,3586,463],{"class":303},[283,3588,1277],{"class":289},[283,3590,3126],{"class":299},[283,3592,463],{"class":303},[283,3594,3131],{"class":895},[283,3596,3101],{"class":303},[283,3598,3599],{"class":285,"line":1530},[283,3600,543],{"emptyLinePlaceholder":542},[283,3602,3603,3605,3607],{"class":285,"line":1535},[283,3604,2947],{"class":303},[283,3606,3209],{"class":299},[283,3608,613],{"class":303},[283,3610,3611],{"class":285,"line":1559},[283,3612,3217],{"class":303},[283,3614,3615],{"class":285,"line":1569},[283,3616,440],{"class":303},[10,3618,3619,3622,3623,3626,3627,254,3630,3633,3634,109,3637,60],{},[14,3620,3621],{},"xhr.responseType = 'blob'"," is the critical line for file downloads. Without it, you get text, and for binary content that means the browser decodes bytes as UTF-8 and hands you garbage. For JSON you'd use ",[14,3624,3625],{},"'json'",", for arbitrary bytes ",[14,3628,3629],{},"'arraybuffer'",[14,3631,3632],{},"'blob'",". Set it ",[1017,3635,3636],{},"before",[14,3638,3639],{},"send()",[10,3641,3642,3643,3645,3646,3648,3649,3651,3652,3654,3655,3657],{},"The server has to send a ",[14,3644,2669],{}," header if you want a meaningful ",[14,3647,2676],{},". Chunked transfer encoding doesn't include one, and ",[14,3650,2587],{}," will be ",[14,3653,2665],{},". CDNs that gzip on the fly also sometimes strip ",[14,3656,2669],{},". When that happens, you're stuck with a bytes counter and no percentage. Which is honest.",[94,3659,3661,3662],{"id":3660},"wrapping-xhr-to-look-like-fetch","Wrapping XHR to look like ",[14,3663,16],{},[10,3665,3666,3667,3669,3670,3672],{},"Most of a modern codebase is ",[14,3668,16],{},"-shaped. If you do real progress in XHR but everything else in ",[14,3671,16],{},", you end up with two parallel request pipelines: different error types, different abort semantics, different header normalization.",[10,3674,3675,3676,3678,3679,3684],{},"The trick is to let ",[14,3677,16],{}," be the default and swap in an XHR-backed function ",[1017,3680,3681,3682],{},"that returns a ",[14,3683,28],{}," when progress is needed. The calling code doesn't change.",[10,3686,3687],{},"Here's a wrapper that does exactly that (this is the real implementation from my api-provider package):",[274,3689,3691],{"className":276,"code":3690,"language":278,"meta":279,"style":279},"export function createXhrFetch(\n  onProgress: (progress: RequestProgress) => void\n): (input: string, init: RequestInit) => Promise\u003CResponse> {\n  return function xhrFetch(input, init) {\n    const method = (init.method ?? 'GET').toUpperCase();\n    const body = (init.body ?? null) as XMLHttpRequestBodyInit | null;\n    const headers = init.headers as Record\u003Cstring, string> | undefined;\n    const signal = init.signal as AbortSignal | null | undefined;\n\n    return new Promise\u003CResponse>((resolve, reject) => {\n      if (typeof XMLHttpRequest === 'undefined') {\n        reject(new Error('XHR unavailable in this runtime'));\n        return;\n      }\n\n      const xhr = new XMLHttpRequest();\n      xhr.open(method, input, true);\n      xhr.responseType = 'blob';\n      if (init.credentials === 'include') xhr.withCredentials = true;\n\n      if (headers) {\n        for (const [name, value] of Object.entries(headers)) {\n          try { xhr.setRequestHeader(name, value); }\n          catch { \u002F* forbidden headers are silently skipped *\u002F }\n        }\n      }\n\n      xhr.upload.onprogress = (e) => onProgress(normaliseProgress('upload', e));\n      xhr.onprogress        = (e) => onProgress(normaliseProgress('download', e));\n\n      const onAbort = () => xhr.abort();\n      if (signal) {\n        if (signal.aborted) {\n          reject(new DOMException('Aborted', 'AbortError'));\n          return;\n        }\n        signal.addEventListener('abort', onAbort, { once: true });\n      }\n\n      xhr.onload = () => {\n        if (signal) signal.removeEventListener('abort', onAbort);\n        resolve(new Response(xhr.response as BodyInit | null, {\n          status: xhr.status,\n          statusText: xhr.statusText,\n          headers: parseHeaders(xhr.getAllResponseHeaders()),\n        }));\n      };\n      xhr.onerror   = () => reject(new TypeError('Network error'));\n      xhr.onabort   = () => reject(new DOMException('Aborted', 'AbortError'));\n      xhr.ontimeout = () => reject(new DOMException('Request timeout', 'TimeoutError'));\n\n      xhr.send(body);\n    });\n  };\n}\n",[14,3692,3693,3704,3725,3760,3779,3804,3833,3866,3891,3895,3921,3941,3958,3965,3969,3973,3987,4001,4012,4034,4038,4045,4079,4093,4106,4111,4115,4119,4152,4181,4185,4205,4213,4222,4244,4252,4257,4275,4280,4285,4300,4318,4343,4349,4355,4373,4379,4385,4413,4444,4477,4482,4492,4498,4503],{"__ignoreMap":279},[283,3694,3695,3697,3699,3702],{"class":285,"line":286},[283,3696,290],{"class":289},[283,3698,296],{"class":289},[283,3700,3701],{"class":299}," createXhrFetch",[283,3703,2837],{"class":303},[283,3705,3706,3708,3710,3712,3714,3716,3719,3721,3723],{"class":285,"line":352},[283,3707,2335],{"class":299},[283,3709,317],{"class":289},[283,3711,378],{"class":303},[283,3713,48],{"class":313},[283,3715,317],{"class":289},[283,3717,3718],{"class":299}," RequestProgress",[283,3720,1116],{"class":303},[283,3722,949],{"class":289},[283,3724,2883],{"class":320},[283,3726,3727,3729,3731,3733,3736,3738,3740,3742,3744,3746,3748,3750,3752,3754,3756,3758],{"class":285,"line":372},[283,3728,841],{"class":303},[283,3730,317],{"class":289},[283,3732,378],{"class":303},[283,3734,3735],{"class":313},"input",[283,3737,317],{"class":289},[283,3739,321],{"class":320},[283,3741,180],{"class":303},[283,3743,326],{"class":313},[283,3745,317],{"class":289},[283,3747,331],{"class":299},[283,3749,1116],{"class":303},[283,3751,949],{"class":289},[283,3753,342],{"class":299},[283,3755,304],{"class":303},[283,3757,28],{"class":299},[283,3759,349],{"class":303},[283,3761,3762,3764,3766,3769,3771,3773,3775,3777],{"class":285,"line":411},[283,3763,414],{"class":289},[283,3765,296],{"class":289},[283,3767,3768],{"class":299}," xhrFetch",[283,3770,463],{"class":303},[283,3772,3735],{"class":313},[283,3774,180],{"class":303},[283,3776,326],{"class":313},[283,3778,1899],{"class":303},[283,3780,3781,3783,3786,3788,3791,3793,3796,3799,3802],{"class":285,"line":437},[283,3782,594],{"class":289},[283,3784,3785],{"class":320}," method",[283,3787,334],{"class":289},[283,3789,3790],{"class":303}," (init.method ",[283,3792,1305],{"class":289},[283,3794,3795],{"class":895}," 'GET'",[283,3797,3798],{"class":303},").",[283,3800,3801],{"class":299},"toUpperCase",[283,3803,613],{"class":303},[283,3805,3806,3808,3811,3813,3816,3818,3820,3822,3824,3827,3829,3831],{"class":285,"line":539},[283,3807,594],{"class":289},[283,3809,3810],{"class":320}," body",[283,3812,334],{"class":289},[283,3814,3815],{"class":303}," (init.body ",[283,3817,1305],{"class":289},[283,3819,2232],{"class":320},[283,3821,1116],{"class":303},[283,3823,425],{"class":289},[283,3825,3826],{"class":299}," XMLHttpRequestBodyInit",[283,3828,848],{"class":289},[283,3830,2232],{"class":320},[283,3832,483],{"class":303},[283,3834,3835,3837,3839,3841,3844,3846,3848,3850,3852,3854,3856,3859,3862,3864],{"class":285,"line":546},[283,3836,594],{"class":289},[283,3838,621],{"class":320},[283,3840,334],{"class":289},[283,3842,3843],{"class":303}," init.headers ",[283,3845,425],{"class":289},[283,3847,494],{"class":299},[283,3849,304],{"class":303},[283,3851,499],{"class":320},[283,3853,180],{"class":303},[283,3855,499],{"class":320},[283,3857,3858],{"class":303},"> ",[283,3860,3861],{"class":289},"|",[283,3863,851],{"class":320},[283,3865,483],{"class":303},[283,3867,3868,3870,3872,3874,3877,3879,3881,3883,3885,3887,3889],{"class":285,"line":591},[283,3869,594],{"class":289},[283,3871,772],{"class":320},[283,3873,334],{"class":289},[283,3875,3876],{"class":303}," init.signal ",[283,3878,425],{"class":289},[283,3880,829],{"class":299},[283,3882,848],{"class":289},[283,3884,2232],{"class":320},[283,3886,848],{"class":289},[283,3888,851],{"class":320},[283,3890,483],{"class":303},[283,3892,3893],{"class":285,"line":616},[283,3894,543],{"emptyLinePlaceholder":542},[283,3896,3897,3899,3901,3903,3905,3907,3909,3911,3913,3915,3917,3919],{"class":285,"line":654},[283,3898,706],{"class":289},[283,3900,390],{"class":289},[283,3902,342],{"class":320},[283,3904,304],{"class":303},[283,3906,28],{"class":299},[283,3908,2912],{"class":303},[283,3910,2915],{"class":313},[283,3912,180],{"class":303},[283,3914,2920],{"class":313},[283,3916,1116],{"class":303},[283,3918,949],{"class":289},[283,3920,471],{"class":303},[283,3922,3923,3925,3927,3930,3933,3936,3939],{"class":285,"line":675},[283,3924,1943],{"class":289},[283,3926,378],{"class":303},[283,3928,3929],{"class":289},"typeof",[283,3931,3932],{"class":303}," XMLHttpRequest ",[283,3934,3935],{"class":289},"===",[283,3937,3938],{"class":895}," 'undefined'",[283,3940,1899],{"class":303},[283,3942,3943,3945,3947,3949,3951,3953,3956],{"class":285,"line":703},[283,3944,3541],{"class":299},[283,3946,463],{"class":303},[283,3948,1277],{"class":289},[283,3950,3084],{"class":299},[283,3952,463],{"class":303},[283,3954,3955],{"class":895},"'XHR unavailable in this runtime'",[283,3957,3101],{"class":303},[283,3959,3960,3963],{"class":285,"line":725},[283,3961,3962],{"class":289},"        return",[283,3964,483],{"class":303},[283,3966,3967],{"class":285,"line":731},[283,3968,3566],{"class":303},[283,3970,3971],{"class":285,"line":1377},[283,3972,543],{"emptyLinePlaceholder":542},[283,3974,3975,3977,3979,3981,3983,3985],{"class":285,"line":1402},[283,3976,2994],{"class":289},[283,3978,2933],{"class":320},[283,3980,334],{"class":289},[283,3982,390],{"class":289},[283,3984,2940],{"class":299},[283,3986,613],{"class":303},[283,3988,3989,3992,3994,3997,3999],{"class":285,"line":1429},[283,3990,3991],{"class":303},"      xhr.",[283,3993,2950],{"class":299},[283,3995,3996],{"class":303},"(method, input, ",[283,3998,976],{"class":320},[283,4000,788],{"class":303},[283,4002,4003,4006,4008,4010],{"class":285,"line":1450},[283,4004,4005],{"class":303},"      xhr.responseType ",[283,4007,533],{"class":289},[283,4009,3419],{"class":895},[283,4011,483],{"class":303},[283,4013,4014,4016,4019,4021,4024,4027,4029,4032],{"class":285,"line":1456},[283,4015,1943],{"class":289},[283,4017,4018],{"class":303}," (init.credentials ",[283,4020,3935],{"class":289},[283,4022,4023],{"class":895}," 'include'",[283,4025,4026],{"class":303},") xhr.withCredentials ",[283,4028,533],{"class":289},[283,4030,4031],{"class":320}," true",[283,4033,483],{"class":303},[283,4035,4036],{"class":285,"line":1461},[283,4037,543],{"emptyLinePlaceholder":542},[283,4039,4040,4042],{"class":285,"line":1487},[283,4041,1943],{"class":289},[283,4043,4044],{"class":303}," (headers) {\n",[283,4046,4047,4050,4052,4054,4057,4060,4062,4064,4067,4070,4073,4076],{"class":285,"line":1499},[283,4048,4049],{"class":289},"        for",[283,4051,378],{"class":303},[283,4053,769],{"class":289},[283,4055,4056],{"class":303}," [",[283,4058,4059],{"class":320},"name",[283,4061,180],{"class":303},[283,4063,1689],{"class":320},[283,4065,4066],{"class":303},"] ",[283,4068,4069],{"class":289},"of",[283,4071,4072],{"class":303}," Object.",[283,4074,4075],{"class":299},"entries",[283,4077,4078],{"class":303},"(headers)) {\n",[283,4080,4081,4084,4087,4090],{"class":285,"line":1530},[283,4082,4083],{"class":289},"          try",[283,4085,4086],{"class":303}," { xhr.",[283,4088,4089],{"class":299},"setRequestHeader",[283,4091,4092],{"class":303},"(name, value); }\n",[283,4094,4095,4098,4100,4103],{"class":285,"line":1535},[283,4096,4097],{"class":289},"          catch",[283,4099,517],{"class":303},[283,4101,4102],{"class":917},"\u002F* forbidden headers are silently skipped *\u002F",[283,4104,4105],{"class":303}," }\n",[283,4107,4108],{"class":285,"line":1559},[283,4109,4110],{"class":303},"        }\n",[283,4112,4113],{"class":285,"line":1569},[283,4114,3566],{"class":303},[283,4116,4117],{"class":285,"line":1596},[283,4118,543],{"emptyLinePlaceholder":542},[283,4120,4121,4124,4126,4128,4130,4132,4134,4136,4139,4141,4144,4146,4149],{"class":285,"line":1601},[283,4122,4123],{"class":303},"      xhr.upload.",[283,4125,2391],{"class":299},[283,4127,334],{"class":289},[283,4129,378],{"class":303},[283,4131,2398],{"class":313},[283,4133,1116],{"class":303},[283,4135,949],{"class":289},[283,4137,4138],{"class":299}," onProgress",[283,4140,463],{"class":303},[283,4142,4143],{"class":299},"normaliseProgress",[283,4145,463],{"class":303},[283,4147,4148],{"class":895},"'upload'",[283,4150,4151],{"class":303},", e));\n",[283,4153,4154,4156,4158,4160,4162,4164,4166,4168,4170,4172,4174,4176,4179],{"class":285,"line":1606},[283,4155,3991],{"class":303},[283,4157,2391],{"class":299},[283,4159,2424],{"class":289},[283,4161,378],{"class":303},[283,4163,2398],{"class":313},[283,4165,1116],{"class":303},[283,4167,949],{"class":289},[283,4169,4138],{"class":299},[283,4171,463],{"class":303},[283,4173,4143],{"class":299},[283,4175,463],{"class":303},[283,4177,4178],{"class":895},"'download'",[283,4180,4151],{"class":303},[283,4182,4183],{"class":285,"line":1614},[283,4184,543],{"emptyLinePlaceholder":542},[283,4186,4188,4190,4192,4194,4196,4198,4201,4203],{"class":285,"line":4187},31,[283,4189,2994],{"class":289},[283,4191,941],{"class":299},[283,4193,334],{"class":289},[283,4195,946],{"class":303},[283,4197,949],{"class":289},[283,4199,4200],{"class":303}," xhr.",[283,4202,955],{"class":299},[283,4204,613],{"class":303},[283,4206,4208,4210],{"class":285,"line":4207},32,[283,4209,1943],{"class":289},[283,4211,4212],{"class":303}," (signal) {\n",[283,4214,4216,4219],{"class":285,"line":4215},33,[283,4217,4218],{"class":289},"        if",[283,4220,4221],{"class":303}," (signal.aborted) {\n",[283,4223,4225,4228,4230,4232,4234,4236,4238,4240,4242],{"class":285,"line":4224},34,[283,4226,4227],{"class":299},"          reject",[283,4229,463],{"class":303},[283,4231,1277],{"class":289},[283,4233,3154],{"class":299},[283,4235,463],{"class":303},[283,4237,3159],{"class":895},[283,4239,180],{"class":303},[283,4241,3164],{"class":895},[283,4243,3101],{"class":303},[283,4245,4247,4250],{"class":285,"line":4246},35,[283,4248,4249],{"class":289},"          return",[283,4251,483],{"class":303},[283,4253,4255],{"class":285,"line":4254},36,[283,4256,4110],{"class":303},[283,4258,4260,4263,4265,4267,4269,4271,4273],{"class":285,"line":4259},37,[283,4261,4262],{"class":303},"        signal.",[283,4264,965],{"class":299},[283,4266,463],{"class":303},[283,4268,970],{"class":895},[283,4270,973],{"class":303},[283,4272,976],{"class":320},[283,4274,979],{"class":303},[283,4276,4278],{"class":285,"line":4277},38,[283,4279,3566],{"class":303},[283,4281,4283],{"class":285,"line":4282},39,[283,4284,543],{"emptyLinePlaceholder":542},[283,4286,4288,4290,4292,4294,4296,4298],{"class":285,"line":4287},40,[283,4289,3991],{"class":303},[283,4291,2450],{"class":299},[283,4293,334],{"class":289},[283,4295,946],{"class":303},[283,4297,949],{"class":289},[283,4299,471],{"class":303},[283,4301,4303,4305,4308,4311,4313,4315],{"class":285,"line":4302},41,[283,4304,4218],{"class":289},[283,4306,4307],{"class":303}," (signal) signal.",[283,4309,4310],{"class":299},"removeEventListener",[283,4312,463],{"class":303},[283,4314,970],{"class":895},[283,4316,4317],{"class":303},", onAbort);\n",[283,4319,4321,4323,4325,4327,4329,4331,4333,4336,4338,4340],{"class":285,"line":4320},42,[283,4322,3516],{"class":299},[283,4324,463],{"class":303},[283,4326,1277],{"class":289},[283,4328,1113],{"class":299},[283,4330,3519],{"class":303},[283,4332,425],{"class":289},[283,4334,4335],{"class":299}," BodyInit",[283,4337,848],{"class":289},[283,4339,2232],{"class":320},[283,4341,4342],{"class":303},", {\n",[283,4344,4346],{"class":285,"line":4345},43,[283,4347,4348],{"class":303},"          status: xhr.status,\n",[283,4350,4352],{"class":285,"line":4351},44,[283,4353,4354],{"class":303},"          statusText: xhr.statusText,\n",[283,4356,4358,4361,4364,4367,4370],{"class":285,"line":4357},45,[283,4359,4360],{"class":303},"          headers: ",[283,4362,4363],{"class":299},"parseHeaders",[283,4365,4366],{"class":303},"(xhr.",[283,4368,4369],{"class":299},"getAllResponseHeaders",[283,4371,4372],{"class":303},"()),\n",[283,4374,4376],{"class":285,"line":4375},46,[283,4377,4378],{"class":303},"        }));\n",[283,4380,4382],{"class":285,"line":4381},47,[283,4383,4384],{"class":303},"      };\n",[283,4386,4388,4390,4392,4395,4397,4399,4401,4403,4405,4407,4409,4411],{"class":285,"line":4387},48,[283,4389,3991],{"class":303},[283,4391,2471],{"class":299},[283,4393,4394],{"class":289},"   =",[283,4396,946],{"class":303},[283,4398,949],{"class":289},[283,4400,3077],{"class":299},[283,4402,463],{"class":303},[283,4404,1277],{"class":289},[283,4406,3126],{"class":299},[283,4408,463],{"class":303},[283,4410,3131],{"class":895},[283,4412,3101],{"class":303},[283,4414,4416,4418,4420,4422,4424,4426,4428,4430,4432,4434,4436,4438,4440,4442],{"class":285,"line":4415},49,[283,4417,3991],{"class":303},[283,4419,2492],{"class":299},[283,4421,4394],{"class":289},[283,4423,946],{"class":303},[283,4425,949],{"class":289},[283,4427,3077],{"class":299},[283,4429,463],{"class":303},[283,4431,1277],{"class":289},[283,4433,3154],{"class":299},[283,4435,463],{"class":303},[283,4437,3159],{"class":895},[283,4439,180],{"class":303},[283,4441,3164],{"class":895},[283,4443,3101],{"class":303},[283,4445,4447,4449,4451,4453,4455,4457,4459,4461,4463,4465,4467,4470,4472,4475],{"class":285,"line":4446},50,[283,4448,3991],{"class":303},[283,4450,2512],{"class":299},[283,4452,334],{"class":289},[283,4454,946],{"class":303},[283,4456,949],{"class":289},[283,4458,3077],{"class":299},[283,4460,463],{"class":303},[283,4462,1277],{"class":289},[283,4464,3154],{"class":299},[283,4466,463],{"class":303},[283,4468,4469],{"class":895},"'Request timeout'",[283,4471,180],{"class":303},[283,4473,4474],{"class":895},"'TimeoutError'",[283,4476,3101],{"class":303},[283,4478,4480],{"class":285,"line":4479},51,[283,4481,543],{"emptyLinePlaceholder":542},[283,4483,4485,4487,4489],{"class":285,"line":4484},52,[283,4486,3991],{"class":303},[283,4488,3209],{"class":299},[283,4490,4491],{"class":303},"(body);\n",[283,4493,4495],{"class":285,"line":4494},53,[283,4496,4497],{"class":303},"    });\n",[283,4499,4501],{"class":285,"line":4500},54,[283,4502,728],{"class":303},[283,4504,4506],{"class":285,"line":4505},55,[283,4507,440],{"class":303},[10,4509,4510],{},"Four things are doing real work in this shape:",[10,4512,2580,4513,4518,4519,4522],{},[106,4514,4515,4516],{},"return type is ",[14,4517,116],{},". That's the whole point: anything that accepts ",[14,4520,4521],{},"typeof fetch"," accepts this function. The rest of the codebase (interceptors, retry, JSON parsing, error mapping) stays transport-agnostic.",[10,4524,2580,4525,4528,4529,4532,4533,4536,4537,4539,4540,317],{},[106,4526,4527],{},"header normalizer at the bottom"," translates ",[14,4530,4531],{},"xhr.getAllResponseHeaders()"," (a raw CRLF-separated string) into a ",[14,4534,4535],{},"Headers"," object so the ",[14,4538,28],{}," behaves exactly like one from ",[14,4541,16],{},[274,4543,4545],{"className":276,"code":4544,"language":278,"meta":279,"style":279},"function parseHeaders(raw: string): Headers {\n  const out = new Headers();\n  if (!raw) return out;\n  for (const line of raw.trim().split(\u002F[\\r\\n]+\u002F)) {\n    const idx = line.indexOf(':');\n    if (idx \u003C 0) continue;\n    const name = line.slice(0, idx).trim();\n    const value = line.slice(idx + 1).trim();\n    if (name) out.append(name, value);\n  }\n  return out;\n}\n",[14,4546,4547,4572,4587,4603,4643,4664,4682,4707,4734,4746,4750,4756],{"__ignoreMap":279},[283,4548,4549,4551,4554,4556,4559,4561,4563,4565,4567,4570],{"class":285,"line":286},[283,4550,816],{"class":289},[283,4552,4553],{"class":299}," parseHeaders",[283,4555,463],{"class":303},[283,4557,4558],{"class":313},"raw",[283,4560,317],{"class":289},[283,4562,321],{"class":320},[283,4564,841],{"class":303},[283,4566,317],{"class":289},[283,4568,4569],{"class":299}," Headers",[283,4571,471],{"class":303},[283,4573,4574,4576,4579,4581,4583,4585],{"class":285,"line":352},[283,4575,355],{"class":289},[283,4577,4578],{"class":320}," out",[283,4580,334],{"class":289},[283,4582,390],{"class":289},[283,4584,4569],{"class":299},[283,4586,613],{"class":303},[283,4588,4589,4591,4593,4595,4598,4600],{"class":285,"line":372},[283,4590,375],{"class":289},[283,4592,378],{"class":303},[283,4594,381],{"class":289},[283,4596,4597],{"class":303},"raw) ",[283,4599,867],{"class":289},[283,4601,4602],{"class":303}," out;\n",[283,4604,4605,4607,4609,4611,4614,4616,4619,4622,4625,4628,4630,4632,4635,4638,4640],{"class":285,"line":411},[283,4606,1871],{"class":289},[283,4608,378],{"class":303},[283,4610,769],{"class":289},[283,4612,4613],{"class":320}," line",[283,4615,1347],{"class":289},[283,4617,4618],{"class":303}," raw.",[283,4620,4621],{"class":299},"trim",[283,4623,4624],{"class":303},"().",[283,4626,4627],{"class":299},"split",[283,4629,463],{"class":303},[283,4631,25],{"class":895},[283,4633,4634],{"class":320},"[\\r\\n]",[283,4636,4637],{"class":289},"+",[283,4639,25],{"class":895},[283,4641,4642],{"class":303},")) {\n",[283,4644,4645,4647,4650,4652,4655,4657,4659,4662],{"class":285,"line":437},[283,4646,594],{"class":289},[283,4648,4649],{"class":320}," idx",[283,4651,334],{"class":289},[283,4653,4654],{"class":303}," line.",[283,4656,1519],{"class":299},[283,4658,463],{"class":303},[283,4660,4661],{"class":895},"':'",[283,4663,788],{"class":303},[283,4665,4666,4668,4671,4673,4675,4677,4680],{"class":285,"line":539},[283,4667,678],{"class":289},[283,4669,4670],{"class":303}," (idx ",[283,4672,304],{"class":289},[283,4674,1884],{"class":320},[283,4676,1116],{"class":303},[283,4678,4679],{"class":289},"continue",[283,4681,483],{"class":303},[283,4683,4684,4686,4689,4691,4693,4696,4698,4700,4703,4705],{"class":285,"line":546},[283,4685,594],{"class":289},[283,4687,4688],{"class":320}," name",[283,4690,334],{"class":289},[283,4692,4654],{"class":303},[283,4694,4695],{"class":299},"slice",[283,4697,463],{"class":303},[283,4699,2680],{"class":320},[283,4701,4702],{"class":303},", idx).",[283,4704,4621],{"class":299},[283,4706,613],{"class":303},[283,4708,4709,4711,4714,4716,4718,4720,4723,4725,4728,4730,4732],{"class":285,"line":591},[283,4710,594],{"class":289},[283,4712,4713],{"class":320}," value",[283,4715,334],{"class":289},[283,4717,4654],{"class":303},[283,4719,4695],{"class":299},[283,4721,4722],{"class":303},"(idx ",[283,4724,4637],{"class":289},[283,4726,4727],{"class":320}," 1",[283,4729,3798],{"class":303},[283,4731,4621],{"class":299},[283,4733,613],{"class":303},[283,4735,4736,4738,4741,4743],{"class":285,"line":616},[283,4737,678],{"class":289},[283,4739,4740],{"class":303}," (name) out.",[283,4742,3194],{"class":299},[283,4744,4745],{"class":303},"(name, value);\n",[283,4747,4748],{"class":285,"line":654},[283,4749,1453],{"class":303},[283,4751,4752,4754],{"class":285,"line":675},[283,4753,414],{"class":289},[283,4755,4602],{"class":303},[283,4757,4758],{"class":285,"line":703},[283,4759,440],{"class":303},[10,4761,2580,4762,4765,4766,4768,4769,4772,4773,4776,4777,4780],{},[106,4763,4764],{},"AbortSignal bridge"," converts between ",[14,4767,16],{},"'s cancellation primitive and XHR's ",[14,4770,4771],{},"abort()"," method. ",[14,4774,4775],{},"AbortSignal.addEventListener('abort', xhr.abort)"," makes the two APIs speak the same language; checking ",[14,4778,4779],{},"signal.aborted"," up front handles the case where the caller passes an already-aborted signal (which happens, for example, when you cancel a request before it's sent).",[10,4782,2580,4783,4790,4791,180,4794,180,4797,4800,4801,4804,4805,4808,4809,4811,4812,4814],{},[106,4784,4785,4787,4788],{},[14,4786,1023],{}," around ",[14,4789,4089],{}," is deliberate. Browsers forbid certain headers (",[14,4792,4793],{},"User-Agent",[14,4795,4796],{},"Cookie",[14,4798,4799],{},"Host",", most ",[14,4802,4803],{},"Sec-*"," and ",[14,4806,4807],{},"Proxy-*"," variants). ",[14,4810,16],{}," silently ignores them too, and the ",[14,4813,1023],{}," makes XHR match the behavior.",[94,4816,4818],{"id":4817},"aborting-timing-out-and-erroring","Aborting, timing out, and erroring",[10,4820,4821,4822,180,4824,180,4826,180,4828,4830,4831,4833],{},"The four terminal outcomes of an XHR are ",[14,4823,2450],{},[14,4825,2471],{},[14,4827,2492],{},[14,4829,2512],{},". They're mutually exclusive: exactly one fires per request. The translation to ",[14,4832,16],{},"-world:",[4835,4836,4837,4853],"table",{},[4838,4839,4840],"thead",{},[4841,4842,4843,4847,4850],"tr",{},[4844,4845,4846],"th",{},"XHR event",[4844,4848,4849],{},"Meaning",[4844,4851,4852],{},"Promise shape",[4854,4855,4856,4871,4885,4902],"tbody",{},[4841,4857,4858,4863,4866],{},[4859,4860,4861],"td",{},[14,4862,2450],{},[4859,4864,4865],{},"Server replied (any status)",[4859,4867,4868],{},[14,4869,4870],{},"resolve(new Response(...))",[4841,4872,4873,4877,4880],{},[4859,4874,4875],{},[14,4876,2471],{},[4859,4878,4879],{},"Network failure, CORS block, DNS",[4859,4881,4882],{},[14,4883,4884],{},"reject(new TypeError('Network error'))",[4841,4886,4887,4891,4897],{},[4859,4888,4889],{},[14,4890,2492],{},[4859,4892,4893,4896],{},[14,4894,4895],{},"xhr.abort()"," was called",[4859,4898,4899],{},[14,4900,4901],{},"reject(new DOMException('...', 'AbortError'))",[4841,4903,4904,4908,4914],{},[4859,4905,4906],{},[14,4907,2512],{},[4859,4909,4910,4913],{},[14,4911,4912],{},"xhr.timeout"," ms elapsed",[4859,4915,4916],{},[14,4917,4918],{},"reject(new DOMException('...', 'TimeoutError'))",[10,4920,4921,4923,4924,4927,4928,4931],{},[14,4922,16],{}," uses ",[14,4925,4926],{},"TypeError"," for network errors and ",[14,4929,4930],{},"DOMException"," for abort. Matching those types means existing error handlers keep working when you swap transports.",[10,4933,4934,4935,4938,4939,4941,4942,4944,4945,4944,4947,4950],{},"A caller-set ",[14,4936,4937],{},"xhr.timeout = 30_000"," fires ",[14,4940,2512],{}," automatically without any extra code. That's one of the small wins of XHR over ",[14,4943,16],{}," + ",[14,4946,20],{},[14,4948,4949],{},"setTimeout",": the timeout machinery is built in.",[94,4952,4954],{"id":4953},"pitfalls-worth-naming","Pitfalls worth naming",[10,4956,4957],{},"A handful of traps that cost me time the first time I hit them.",[10,4959,4960,4966,4967,4970],{},[106,4961,4962,4963,4965],{},"Setting ",[14,4964,3235],{}," with FormData breaks multipart."," The browser's automatic ",[14,4968,4969],{},"multipart\u002Fform-data; boundary=..."," header is the only one the server can parse. Don't override it.",[10,4972,4973,4976,4977,4980,4981,4984],{},[106,4974,4975],{},"CORS progress is restricted."," For cross-origin requests, upload progress events only fire if the response includes the appropriate ",[14,4978,4979],{},"Access-Control-Allow-Origin"," header. If you're seeing ",[14,4982,4983],{},"loaded = 0"," forever on a cross-origin upload, it's almost certainly CORS and not your code.",[10,4986,4987,4995],{},[106,4988,4989,4992,4993,60],{},[14,4990,4991],{},"responseType"," must be set before ",[14,4994,3639],{}," Setting it after is a no-op. The browser needs to know how to buffer the response from the first byte.",[10,4997,4998,5003,5004,5006,5007,5010],{},[106,4999,5000,5001,60],{},"Gzip and ",[14,5002,2669],{}," Many CDNs strip ",[14,5005,2669],{}," when they apply gzip on the fly (because the compressed length is different from the decompressed length). Your client sees ",[14,5008,5009],{},"lengthComputable: false"," even though the server knew the size. There's no client-side fix. If you control the server, serve pre-compressed content or skip compression for assets where progress matters.",[10,5012,5013,109,5016,5019,5020,5023,5024,5027],{},[106,5014,5015],{},"Don't hold the whole response in memory unnecessarily.",[14,5017,5018],{},"responseType: 'blob'"," lets the browser manage the buffer; ",[14,5021,5022],{},"responseType: 'arraybuffer'"," forces it into JS-heap memory. For anything above a few MB, ",[14,5025,5026],{},"blob"," is the right default.",[94,5029,5031],{"id":5030},"what-id-reach-for-first","What I'd reach for first",[10,5033,5034],{},"For any project that needs real progress UIs, the shape I'd build every time:",[3226,5036,5037,5045,5055,5063,5076],{},[68,5038,5039,5044],{},[106,5040,5041,5042,60],{},"Default transport is ",[14,5043,16],{}," It's the modern primitive; it's faster; it hydrates into SSR-friendly patterns cleanly.",[68,5046,5047,5050,5051,5054],{},[106,5048,5049],{},"Opt into XHR only when the caller asks for progress."," A single ",[14,5052,5053],{},"onRequestProgress"," option is enough to swap transports for that specific call.",[68,5056,5057,5062],{},[106,5058,5059,5060],{},"The XHR path returns a ",[14,5061,116],{},", so everything downstream keeps working: interceptors, retry, error mapping, JSON parsing.",[68,5064,5065,5072,5073,5075],{},[106,5066,5067,5068,5071],{},"Normalize ",[14,5069,5070],{},"{ loaded, total, ratio }"," at the transport layer."," Hand ",[14,5074,2809],{}," upstream when the total is unknown. Let the UI decide how to render that state.",[68,5077,5078,5084,5085,5087,5088,5090],{},[106,5079,5080,5081,5083],{},"Bridge ",[14,5082,754],{}," once"," in the wrapper, not in every caller. XHR's ",[14,5086,4771],{}," and fetch's ",[14,5089,20],{}," exist in the same logical slot; the wrapper translates.",[10,5092,5093],{},"That's the whole picture. XHR isn't the cool API anymore; it's the one that still works for a specific thing fetch can't do, and the small amount of glue code above is what keeps it from contaminating the rest of the codebase.",[5095,5096,5097],"style",{},"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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":279,"searchDepth":352,"depth":352,"links":5099},[5100,5102,5110,5112,5113,5115,5116,5117,5119,5120,5121],{"id":96,"depth":352,"text":5101},"Why native fetch is the default primitive",{"id":244,"depth":352,"text":245,"children":5103},[5104,5105,5107,5108,5109],{"id":268,"depth":372,"text":269},{"id":750,"depth":372,"text":5106},"Layer 2: timeouts via AbortSignal",{"id":1031,"depth":372,"text":1032},{"id":1780,"depth":372,"text":1781},{"id":2014,"depth":372,"text":2015},{"id":2166,"depth":352,"text":5111},"Why fetch can't show upload progress",{"id":2374,"depth":352,"text":2375},{"id":2579,"depth":352,"text":5114},"The ProgressEvent shape, and lengthComputable",{"id":2816,"depth":352,"text":2817},{"id":3270,"depth":352,"text":3271},{"id":3660,"depth":352,"text":5118},"Wrapping XHR to look like fetch",{"id":4817,"depth":352,"text":4818},{"id":4953,"depth":352,"text":4954},{"id":5030,"depth":352,"text":5031},"2025-09-22","Why native fetch is the right default, how to build a layered wrapper (base URL, timeouts, interceptors, retry), and when you still need XHR.","لماذا يبقى fetch الأصلي الخيار الافتراضي الصحيح، وكيف تبني حوله غلافاً متعدّد الطبقات (رابط أساسي، ومهلات، واعتراضات، وإعادة محاولات)، ومتى تظل بحاجة إلى XHR.",false,"md",null,[5129,55,5130,5131,5132,20,754,5133,5134,5135,5136,5137],"fetch API","XHR","upload progress","download progress","custom HTTP client","interceptors","retry","exponential backoff","TypeScript","en",{},"\u002Fwrites\u002Fupload-and-download-progress-in-the-browser",{"title":5,"description":5123},"writes\u002Fupload-and-download-progress-in-the-browser","غلاف مخصَّص لـ fetch، وXHR لتقدُّم الرفع","vCepZSovWCiH7mQTZEqzp9WPeIcjAJH2ksJYbQ6skaY",[5146,6909],{"id":5147,"title":5148,"body":5149,"date":6891,"description":6892,"description_ar":6893,"draft":5125,"extension":5126,"image":5127,"keywords":6894,"lang":5138,"meta":6903,"navigation":542,"path":6904,"seo":6905,"stem":6906,"title_ar":6907,"updatedAt":5127,"__hash__":6908},"writes\u002Fwrites\u002Finside-a-skeleton-self-generating-skeleton-loaders.md","Inside @alikhalilll\u002Fa-skeleton, self-generating skeleton loaders",{"type":7,"value":5150,"toc":6880},[5151,5154,5160,5163,5167,5170,5175,5225,5230,5261,5289,5295,5309,5312,5316,5323,5344,5351,5362,5365,5406,5409,5495,5523,5529,5533,5540,5585,5610,5616,5619,5623,5632,6013,6050,6053,6122,6125,6242,6245,6268,6271,6275,6278,6284,6320,6339,6356,6365,6369,6417,6420,6427,6503,6516,6520,6563,6569,6580,6663,6670,6777,6799,6802,6806,6825,6843,6850,6854,6865,6871,6874,6877],[10,5152,5153],{},"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,5155,5156,5159],{},[14,5157,5158],{},"@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,5161,5162],{},"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.",[94,5164,5166],{"id":5165},"the-contract-keep-tags-unconditional-gate-content","The contract: keep tags unconditional, gate content",[10,5168,5169],{},"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,5171,5172],{},[106,5173,5174],{},"Wrong:",[274,5176,5180],{"className":5177,"code":5178,"language":5179,"meta":279,"style":279},"language-vue shiki shiki-themes github-light github-dark","\u003Cdiv v-if=\"user\">\n  \u003Ch3>{{ user.name }}\u003C\u002Fh3>\n  \u003Cp>{{ user.bio }}\u003C\u002Fp>\n\u003C\u002Fdiv>\n","vue",[14,5181,5182,5206,5211,5216],{"__ignoreMap":279},[283,5183,5184,5186,5190,5193,5195,5198,5201,5203],{"class":285,"line":286},[283,5185,304],{"class":303},[283,5187,5189],{"class":5188},"s9eBZ","div",[283,5191,5192],{"class":289}," v-if",[283,5194,533],{"class":303},[283,5196,5197],{"class":895},"\"",[283,5199,5200],{"class":303},"user",[283,5202,5197],{"class":895},[283,5204,5205],{"class":303},">\n",[283,5207,5208],{"class":285,"line":352},[283,5209,5210],{"class":303},"  \u003Ch3>{{ user.name }}\u003C\u002Fh3>\n",[283,5212,5213],{"class":285,"line":372},[283,5214,5215],{"class":303},"  \u003Cp>{{ user.bio }}\u003C\u002Fp>\n",[283,5217,5218,5221,5223],{"class":285,"line":411},[283,5219,5220],{"class":303},"\u003C\u002F",[283,5222,5189],{"class":5188},[283,5224,5205],{"class":303},[10,5226,5227],{},[106,5228,5229],{},"Right:",[274,5231,5233],{"className":5177,"code":5232,"language":5179,"meta":279,"style":279},"\u003Cdiv>\n  \u003Ch3>{{ user?.name }}\u003C\u002Fh3>\n  \u003Cp>{{ user?.bio }}\u003C\u002Fp>\n\u003C\u002Fdiv>\n",[14,5234,5235,5243,5248,5253],{"__ignoreMap":279},[283,5236,5237,5239,5241],{"class":285,"line":286},[283,5238,304],{"class":303},[283,5240,5189],{"class":5188},[283,5242,5205],{"class":303},[283,5244,5245],{"class":285,"line":352},[283,5246,5247],{"class":303},"  \u003Ch3>{{ user?.name }}\u003C\u002Fh3>\n",[283,5249,5250],{"class":285,"line":372},[283,5251,5252],{"class":303},"  \u003Cp>{{ user?.bio }}\u003C\u002Fp>\n",[283,5254,5255,5257,5259],{"class":285,"line":411},[283,5256,5220],{"class":303},[283,5258,5189],{"class":5188},[283,5260,5205],{"class":303},[10,5262,5263,5264,5267,5268,5271,5272,5274,5275,5278,5279,180,5282,180,5285,5288],{},"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,5265,5266],{},"v-if=\"user\"",", the walker sees a comment node during loading and an ",[14,5269,5270],{},"\u003Ch3>"," after, and it has no way to predict the placeholder geometry. With the optional-chained interpolation, the ",[14,5273,5270],{}," is always there; its text is empty during loading, and the walker classifies empty text-owner tags (",[14,5276,5277],{},"\u003Ch1>","–",[14,5280,5281],{},"\u003Ch6>",[14,5283,5284],{},"\u003Cp>",[14,5286,5287],{},"\u003Cspan>",", etc.) as shimmer text bars at the heading's natural rendered height.",[10,5290,5291,5292,5294],{},"Tags are the schema. Content is the payload. You can swap the payload to ",[14,5293,2809],{}," without the schema noticing. That single discipline is what unlocks the rest.",[10,5296,5297,5298,5301,5302,5304,5305,5308],{},"The other half of the contract is the ",[14,5299,5300],{},"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,5303,5300],{}," and the walker treats it as invisible; the real element renders verbatim through both states. The corollary, ",[14,5306,5307],{},"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,5310,5311],{},"Two rules, two attributes. That's the whole authoring surface you ever have to learn.",[94,5313,5315],{"id":5314},"clone-mode-snapshot-the-final-paint-replay-the-pixels","Clone mode: snapshot the final paint, replay the pixels",[10,5317,5318,5319,5322],{},"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,5320,5321],{},"rounded-*"," prefix. They guess.",[10,5324,5325,5326,5329,5330,5333,5334,5337,5338,5340,5341,5343],{},"Clone mode stops guessing. It mounts the slot off-screen in a ",[14,5327,5328],{},"visibility: hidden"," capture host, lets the browser do its full layout and style resolution, and then reads the ",[106,5331,5332],{},"final computed result"," for every element via ",[14,5335,5336],{},"getComputedStyle()",". Whatever your styling system is (Tailwind utilities, scoped styles with hashed class names, CSS-in-JS at runtime, DaisyUI semantic tokens, inline ",[14,5339,5095],{}," attributes set by a parent), by the time ",[14,5342,5336],{}," runs, all of it has collapsed into one canonical answer per property. The engine snapshots that answer and replays it.",[10,5345,5346,5347,5350],{},"The captured surface is comprehensive. Per-edge borders (so a card with a different ",[14,5348,5349],{},"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,5352,5353,5354,5357,5358,5361],{},"Text is the part that's easy to get wrong, and the part ",[14,5355,5356],{},"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,5359,5360],{},"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,5363,5364],{},"The wrapper API for all of this is one prop:",[274,5366,5368],{"className":5177,"code":5367,"language":5179,"meta":279,"style":279},"\u003CASkeleton :loading=\"loading\">\n  \u003CSomeRichComponent \u002F>\n\u003C\u002FASkeleton>\n",[14,5369,5370,5393,5398],{"__ignoreMap":279},[283,5371,5372,5374,5377,5380,5383,5385,5387,5389,5391],{"class":285,"line":286},[283,5373,304],{"class":303},[283,5375,5376],{"class":5188},"ASkeleton",[283,5378,5379],{"class":303}," :",[283,5381,5382],{"class":299},"loading",[283,5384,533],{"class":303},[283,5386,5197],{"class":895},[283,5388,5382],{"class":303},[283,5390,5197],{"class":895},[283,5392,5205],{"class":303},[283,5394,5395],{"class":285,"line":352},[283,5396,5397],{"class":303},"  \u003CSomeRichComponent \u002F>\n",[283,5399,5400,5402,5404],{"class":285,"line":372},[283,5401,5220],{"class":303},[283,5403,5376],{"class":5188},[283,5405,5205],{"class":303},[10,5407,5408],{},"If you want to roll your own capture \u002F replay flow, the underlying function is a public export:",[274,5410,5414],{"className":5411,"code":5412,"language":5413,"meta":279,"style":279},"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,5415,5416,5437,5441,5461,5471,5481,5491],{"__ignoreMap":279},[283,5417,5418,5421,5424,5426,5429,5432,5435],{"class":285,"line":286},[283,5419,5420],{"class":289},"import",[283,5422,5423],{"class":303}," { captureSnapshot, ",[283,5425,1045],{"class":289},[283,5427,5428],{"class":303}," CaptureSnapshot } ",[283,5430,5431],{"class":289},"from",[283,5433,5434],{"class":895}," '@alikhalilll\u002Fa-skeleton'",[283,5436,483],{"class":303},[283,5438,5439],{"class":285,"line":352},[283,5440,543],{"emptyLinePlaceholder":542},[283,5442,5443,5445,5448,5450,5453,5455,5458],{"class":285,"line":372},[283,5444,769],{"class":289},[283,5446,5447],{"class":320}," snap",[283,5449,317],{"class":289},[283,5451,5452],{"class":299}," CaptureSnapshot",[283,5454,334],{"class":289},[283,5456,5457],{"class":299}," captureSnapshot",[283,5459,5460],{"class":303},"(myRootEl, {\n",[283,5462,5463,5466,5469],{"class":285,"line":411},[283,5464,5465],{"class":303},"  maxDepth: ",[283,5467,5468],{"class":320},"12",[283,5470,2048],{"class":303},[283,5472,5473,5476,5479],{"class":285,"line":437},[283,5474,5475],{"class":303},"  maxNodes: ",[283,5477,5478],{"class":320},"800",[283,5480,2048],{"class":303},[283,5482,5483,5486,5489],{"class":285,"line":539},[283,5484,5485],{"class":303},"  minSize: ",[283,5487,5488],{"class":320},"4",[283,5490,2048],{"class":303},[283,5492,5493],{"class":285,"line":546},[283,5494,1700],{"class":303},[10,5496,5497,180,5500,5503,5504,5507,5508,5511,5512,5515,5516,5519,5520,5522],{},[14,5498,5499],{},"maxDepth",[14,5501,5502],{},"maxNodes",", and ",[14,5505,5506],{},"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,5509,5510],{},"truncated: true",", and the wrapper logs a one-time ",[14,5513,5514],{},"console.warn"," per ",[14,5517,5518],{},"cacheKey"," so you notice during development instead of in production. ",[14,5521,5506],{}," 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,5524,5525,5526,5528],{},"The trade-off clone mode pays is that it's client-side only. You need a real DOM, ",[14,5527,5336],{},", 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.",[94,5530,5532],{"id":5531},"mirror-mode-walk-the-vnodes-ship-to-the-server","Mirror mode: walk the vnodes, ship to the server",[10,5534,5535,5536,5539],{},"Mirror swaps the engine. Instead of mounting the slot and reading the DOM, it walks the vnode tree at render time. No ",[14,5537,5538],{},"window"," access. Hydration-clean. Server-renderable.",[274,5541,5543],{"className":5177,"code":5542,"language":5179,"meta":279,"style":279},"\u003CASkeleton mode=\"mirror\" :loading=\"loading\">\n  \u003CSomeRichComponent \u002F>\n\u003C\u002FASkeleton>\n",[14,5544,5545,5573,5577],{"__ignoreMap":279},[283,5546,5547,5549,5551,5554,5556,5559,5561,5563,5565,5567,5569,5571],{"class":285,"line":286},[283,5548,304],{"class":303},[283,5550,5376],{"class":5188},[283,5552,5553],{"class":299}," mode",[283,5555,533],{"class":303},[283,5557,5558],{"class":895},"\"mirror\"",[283,5560,5379],{"class":303},[283,5562,5382],{"class":299},[283,5564,533],{"class":303},[283,5566,5197],{"class":895},[283,5568,5382],{"class":303},[283,5570,5197],{"class":895},[283,5572,5205],{"class":303},[283,5574,5575],{"class":285,"line":352},[283,5576,5397],{"class":303},[283,5578,5579,5581,5583],{"class":285,"line":372},[283,5580,5220],{"class":303},[283,5582,5376],{"class":5188},[283,5584,5205],{"class":303},[10,5586,5587,5588,5591,5592,5595,5596,180,5599,180,5602,5605,5606,5609],{},"The walker preserves every element's tag and class. Text-bearing leaves become ",[14,5589,5590],{},"\u003Cspan class=\"a-skel-text-content\">"," (transparent text on a skeleton background) with ",[14,5593,5594],{},"box-decoration-break: clone"," so the shimmer wraps the exact rendered text width per line. Atomic and interactive tags (",[14,5597,5598],{},"\u003Cimg>",[14,5600,5601],{},"\u003Cbutton>",[14,5603,5604],{},"\u003Csvg>",") become ",[14,5607,5608],{},"\u003Cdiv class=\"a-skel-block\">"," sized from their class-derived dimensions.",[10,5611,5612,5613,5615],{},"Mirror's win is SSR. Its trade-off is fidelity. Without ",[14,5614,5336],{},", 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,5617,5618],{},"Both modes share the same animation pipeline, the same cache, the same a11y baseline. Switching between them is a single prop.",[94,5620,5622],{"id":5621},"structural-mode-trees-in-normal-flow-persisted-between-sessions","Structural mode: trees in normal flow, persisted between sessions",[10,5624,5625,5626,4944,5629,60],{},"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,5627,5628],{},"useSkeleton()",[14,5630,5631],{},"\u003CASkeletonLayer>",[274,5633,5635],{"className":5177,"code":5634,"language":5179,"meta":279,"style":279},"\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,5636,5637,5657,5671,5684,5688,5713,5731,5757,5783,5787,5810,5829,5851,5860,5864,5868,5896,5904,5908,5917,5933,5959,5976,5996,6005],{"__ignoreMap":279},[283,5638,5639,5641,5644,5647,5650,5652,5655],{"class":285,"line":286},[283,5640,304],{"class":303},[283,5642,5643],{"class":5188},"script",[283,5645,5646],{"class":299}," setup",[283,5648,5649],{"class":299}," lang",[283,5651,533],{"class":303},[283,5653,5654],{"class":895},"\"ts\"",[283,5656,5205],{"class":303},[283,5658,5659,5661,5664,5666,5669],{"class":285,"line":352},[283,5660,5420],{"class":289},[283,5662,5663],{"class":303}," { computed, ref } ",[283,5665,5431],{"class":289},[283,5667,5668],{"class":895}," 'vue'",[283,5670,483],{"class":303},[283,5672,5673,5675,5678,5680,5682],{"class":285,"line":372},[283,5674,5420],{"class":289},[283,5676,5677],{"class":303}," { useSkeleton } ",[283,5679,5431],{"class":289},[283,5681,5434],{"class":895},[283,5683,483],{"class":303},[283,5685,5686],{"class":285,"line":411},[283,5687,543],{"emptyLinePlaceholder":542},[283,5689,5690,5692,5695,5697,5700,5703,5706,5708,5710],{"class":285,"line":437},[283,5691,769],{"class":289},[283,5693,5694],{"class":320}," props",[283,5696,334],{"class":289},[283,5698,5699],{"class":299}," defineProps",[283,5701,5702],{"class":303},"\u003C{ ",[283,5704,5705],{"class":313},"userId",[283,5707,317],{"class":289},[283,5709,321],{"class":320},[283,5711,5712],{"class":303}," }>();\n",[283,5714,5715,5717,5720,5722,5725,5727,5729],{"class":285,"line":539},[283,5716,769],{"class":289},[283,5718,5719],{"class":320}," user",[283,5721,334],{"class":289},[283,5723,5724],{"class":299}," ref",[283,5726,463],{"class":303},[283,5728,2809],{"class":320},[283,5730,788],{"class":303},[283,5732,5733,5735,5738,5740,5743,5746,5748,5751,5753,5755],{"class":285,"line":546},[283,5734,769],{"class":289},[283,5736,5737],{"class":320}," loading",[283,5739,334],{"class":289},[283,5741,5742],{"class":299}," computed",[283,5744,5745],{"class":303},"(() ",[283,5747,949],{"class":289},[283,5749,5750],{"class":303}," user.value ",[283,5752,3935],{"class":289},[283,5754,2232],{"class":320},[283,5756,788],{"class":303},[283,5758,5759,5761,5764,5766,5768,5770,5773,5775,5777,5779,5781],{"class":285,"line":591},[283,5760,769],{"class":289},[283,5762,5763],{"class":320}," containerRef",[283,5765,334],{"class":289},[283,5767,5724],{"class":299},[283,5769,304],{"class":303},[283,5771,5772],{"class":299},"HTMLElement",[283,5774,848],{"class":289},[283,5776,2232],{"class":320},[283,5778,310],{"class":303},[283,5780,2809],{"class":320},[283,5782,788],{"class":303},[283,5784,5785],{"class":285,"line":616},[283,5786,543],{"emptyLinePlaceholder":542},[283,5788,5789,5791,5793,5796,5798,5801,5803,5805,5808],{"class":285,"line":654},[283,5790,769],{"class":289},[283,5792,517],{"class":303},[283,5794,5795],{"class":320},"shape",[283,5797,180],{"class":303},[283,5799,5800],{"class":320},"clear",[283,5802,2295],{"class":303},[283,5804,533],{"class":289},[283,5806,5807],{"class":299}," useSkeleton",[283,5809,2037],{"class":303},[283,5811,5812,5815,5818,5821,5823,5825,5827],{"class":285,"line":675},[283,5813,5814],{"class":303},"  cacheKey: ",[283,5816,5817],{"class":895},"`user-card:${",[283,5819,5820],{"class":303},"props",[283,5822,60],{"class":895},[283,5824,5705],{"class":303},[283,5826,1692],{"class":895},[283,5828,2048],{"class":303},[283,5830,5831,5834,5837,5839,5842,5844,5846,5848],{"class":285,"line":703},[283,5832,5833],{"class":299},"  target",[283,5835,5836],{"class":303},": () ",[283,5838,949],{"class":289},[283,5840,5841],{"class":303}," (loading.value ",[283,5843,2746],{"class":289},[283,5845,2232],{"class":320},[283,5847,5379],{"class":289},[283,5849,5850],{"class":303}," containerRef.value),\n",[283,5852,5853,5856,5858],{"class":285,"line":725},[283,5854,5855],{"class":303},"  persist: ",[283,5857,976],{"class":320},[283,5859,2048],{"class":303},[283,5861,5862],{"class":285,"line":731},[283,5863,1700],{"class":303},[283,5865,5866],{"class":285,"line":1377},[283,5867,543],{"emptyLinePlaceholder":542},[283,5869,5870,5873,5876,5879,5881,5884,5886,5888,5891,5893],{"class":285,"line":1402},[283,5871,5872],{"class":299},"fetchUser",[283,5874,5875],{"class":303},"(props.userId).",[283,5877,5878],{"class":299},"then",[283,5880,1641],{"class":303},[283,5882,5883],{"class":313},"u",[283,5885,1116],{"class":303},[283,5887,949],{"class":289},[283,5889,5890],{"class":303}," (user.value ",[283,5892,533],{"class":289},[283,5894,5895],{"class":303}," u));\n",[283,5897,5898,5900,5902],{"class":285,"line":1429},[283,5899,5220],{"class":303},[283,5901,5643],{"class":5188},[283,5903,5205],{"class":303},[283,5905,5906],{"class":285,"line":1450},[283,5907,543],{"emptyLinePlaceholder":542},[283,5909,5910,5912,5915],{"class":285,"line":1456},[283,5911,304],{"class":303},[283,5913,5914],{"class":5188},"template",[283,5916,5205],{"class":303},[283,5918,5919,5922,5924,5926,5928,5931],{"class":285,"line":1461},[283,5920,5921],{"class":303},"  \u003C",[283,5923,5189],{"class":5188},[283,5925,5724],{"class":299},[283,5927,533],{"class":303},[283,5929,5930],{"class":895},"\"containerRef\"",[283,5932,5205],{"class":303},[283,5934,5935,5938,5941,5943,5945,5948,5951,5953,5956],{"class":285,"line":1487},[283,5936,5937],{"class":303},"    \u003C",[283,5939,5940],{"class":5188},"ASkeletonLayer",[283,5942,5192],{"class":299},[283,5944,533],{"class":303},[283,5946,5947],{"class":895},"\"loading && shape\"",[283,5949,5950],{"class":299}," :shape",[283,5952,533],{"class":303},[283,5954,5955],{"class":895},"\"shape\"",[283,5957,5958],{"class":303}," \u002F>\n",[283,5960,5961,5963,5966,5969,5971,5974],{"class":285,"line":1499},[283,5962,5937],{"class":303},[283,5964,5965],{"class":5188},"ColdStartFallback",[283,5967,5968],{"class":299}," v-else-if",[283,5970,533],{"class":303},[283,5972,5973],{"class":895},"\"loading\"",[283,5975,5958],{"class":303},[283,5977,5978,5980,5983,5986,5989,5991,5994],{"class":285,"line":1530},[283,5979,5937],{"class":303},[283,5981,5982],{"class":5188},"UserCard",[283,5984,5985],{"class":299}," v-else",[283,5987,5988],{"class":299}," :data",[283,5990,533],{"class":303},[283,5992,5993],{"class":895},"\"user\"",[283,5995,5958],{"class":303},[283,5997,5998,6001,6003],{"class":285,"line":1535},[283,5999,6000],{"class":303},"  \u003C\u002F",[283,6002,5189],{"class":5188},[283,6004,5205],{"class":303},[283,6006,6007,6009,6011],{"class":285,"line":1559},[283,6008,5220],{"class":303},[283,6010,5914],{"class":5188},[283,6012,5205],{"class":303},[10,6014,6015,6016,6019,6020,180,6023,180,6026,180,6029,180,6032,180,6035,6038,6039,6041,6042,6045,6046,6049],{},"The shape is captured by ",[14,6017,6018],{},"walkStructural()",", a tree walker that preserves container tags, their original classes, and the resolved layout CSS (",[14,6021,6022],{},"display",[14,6024,6025],{},"flex-*",[14,6027,6028],{},"gap",[14,6030,6031],{},"padding",[14,6033,6034],{},"grid-*",[14,6036,6037],{},"box-sizing","). Leaves carry inline width\u002Fheight plus the visual signals (background, border, radius). When ",[14,6040,5631],{}," replays the tree, it does so in ",[106,6043,6044],{},"normal flow",": the captured ",[14,6047,6048],{},"\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,6051,6052],{},"Given this real markup:",[274,6054,6058],{"className":6055,"code":6056,"language":6057,"meta":279,"style":279},"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,6059,6060,6076,6089,6101,6114],{"__ignoreMap":279},[283,6061,6062,6064,6066,6069,6071,6074],{"class":285,"line":286},[283,6063,304],{"class":303},[283,6065,5189],{"class":5188},[283,6067,6068],{"class":299}," class",[283,6070,533],{"class":303},[283,6072,6073],{"class":895},"\"flex flex-col gap-4 p-4\"",[283,6075,5205],{"class":303},[283,6077,6078,6080,6082,6085,6087],{"class":285,"line":352},[283,6079,5921],{"class":303},[283,6081,266],{"class":5188},[283,6083,6084],{"class":303},">...\u003C\u002F",[283,6086,266],{"class":5188},[283,6088,5205],{"class":303},[283,6090,6091,6093,6095,6097,6099],{"class":285,"line":372},[283,6092,5921],{"class":303},[283,6094,10],{"class":5188},[283,6096,6084],{"class":303},[283,6098,10],{"class":5188},[283,6100,5205],{"class":303},[283,6102,6103,6105,6108,6110,6112],{"class":285,"line":411},[283,6104,5921],{"class":303},[283,6106,6107],{"class":5188},"button",[283,6109,6084],{"class":303},[283,6111,6107],{"class":5188},[283,6113,5205],{"class":303},[283,6115,6116,6118,6120],{"class":285,"line":437},[283,6117,5220],{"class":303},[283,6119,5189],{"class":5188},[283,6121,5205],{"class":303},[10,6123,6124],{},"The layer replays as:",[274,6126,6128],{"className":6055,"code":6127,"language":6057,"meta":279,"style":279},"\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,6129,6130,6137,6147,6157,6161,6188,6211,6234],{"__ignoreMap":279},[283,6131,6132,6134],{"class":285,"line":286},[283,6133,304],{"class":303},[283,6135,6136],{"class":5188},"div\n",[283,6138,6139,6142,6144],{"class":285,"line":352},[283,6140,6141],{"class":299},"  class",[283,6143,533],{"class":303},[283,6145,6146],{"class":895},"\"flex flex-col gap-4 p-4\"\n",[283,6148,6149,6152,6154],{"class":285,"line":372},[283,6150,6151],{"class":299},"  style",[283,6153,533],{"class":303},[283,6155,6156],{"class":895},"\"display: flex; flex-direction: column; gap: 16px; padding: 16px; box-sizing: border-box\"\n",[283,6158,6159],{"class":285,"line":411},[283,6160,5205],{"class":303},[283,6162,6163,6165,6167,6169,6171,6174,6177,6179,6182,6186],{"class":285,"line":437},[283,6164,5921],{"class":303},[283,6166,5189],{"class":5188},[283,6168,6068],{"class":299},[283,6170,533],{"class":303},[283,6172,6173],{"class":895},"\"a-skel\"",[283,6175,6176],{"class":299}," style",[283,6178,533],{"class":303},[283,6180,6181],{"class":895},"\"width: 200px; height: 24px; ...\"",[283,6183,6185],{"class":6184},"s7hpK"," \u002F",[283,6187,5205],{"class":303},[283,6189,6190,6192,6194,6196,6198,6200,6202,6204,6207,6209],{"class":285,"line":539},[283,6191,5921],{"class":303},[283,6193,5189],{"class":5188},[283,6195,6068],{"class":299},[283,6197,533],{"class":303},[283,6199,6173],{"class":895},[283,6201,6176],{"class":299},[283,6203,533],{"class":303},[283,6205,6206],{"class":895},"\"width: 280px; height: 16px; ...\"",[283,6208,6185],{"class":6184},[283,6210,5205],{"class":303},[283,6212,6213,6215,6217,6219,6221,6223,6225,6227,6230,6232],{"class":285,"line":546},[283,6214,5921],{"class":303},[283,6216,5189],{"class":5188},[283,6218,6068],{"class":299},[283,6220,533],{"class":303},[283,6222,6173],{"class":895},[283,6224,6176],{"class":299},[283,6226,533],{"class":303},[283,6228,6229],{"class":895},"\"width: 120px; height: 36px; ...\"",[283,6231,6185],{"class":6184},[283,6233,5205],{"class":303},[283,6235,6236,6238,6240],{"class":285,"line":591},[283,6237,5220],{"class":303},[283,6239,5189],{"class":5188},[283,6241,5205],{"class":303},[10,6243,6244],{},"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,6246,6247,6248,6251,6252,6255,6256,6259,6260,6263,6264,6267],{},"The other thing structural mode gives you is persistence. ",[14,6249,6250],{},"persist: true"," mirrors the captured shape into ",[14,6253,6254],{},"localStorage"," under the ",[14,6257,6258],{},"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,6261,6262],{},"v: 3"," for structural, ",[14,6265,6266],{},"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,6269,6270],{},"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.",[94,6272,6274],{"id":6273},"the-capture-cache-and-replay-pipeline","The capture, cache, and replay pipeline",[10,6276,6277],{},"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,6279,6280,6283],{},[106,6281,6282],{},"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,6285,6286,6289,6290,6293,6294,6297,6298,6301,6302,6304,6305,6307,6308,6311,6312,6314,6315,6317,6318,3798],{},[106,6287,6288],{},"Data arrives."," The slot's real content renders. A ",[14,6291,6292],{},"ResizeObserver"," plus a ",[14,6295,6296],{},"requestAnimationFrame"," schedules a measurement pass. In clone mode, ",[14,6299,6300],{},"captureSnapshot()"," reads ",[14,6303,5336],{}," on every element and per-line text rects via ",[14,6306,5360],{},", then freezes the result into a ",[14,6309,6310],{},"CaptureSnapshot"," tree. In structural mode, ",[14,6313,6018],{}," 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,6316,6254],{}," if ",[14,6319,6250],{},[10,6321,6322,6325,6326,6328,6329,6331,6332,6335,6336,6338],{},[106,6323,6324],{},"Next loading flip."," The user navigates back to the page, or triggers a refetch, or revisits the route on a return session. ",[14,6327,5382],{}," flips to ",[14,6330,976],{},". The cached shape replays instantly, with no intermediate \"guess the shape\" step. In clone mode, ",[14,6333,6334],{},"\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,6337,5631],{}," renders the tree in normal flow, with the captured layout CSS driving the flex\u002Fgrid container behaviour.",[10,6340,6341,6342,6344,6345,6347,6348,6350,6351,5515,6353,6355],{},"The budgets matter at every step. ",[14,6343,5502],{}," (default 500–800 depending on the walker) is the cap on tree size. ",[14,6346,5499],{}," (default 12–16) is the recursion cap. ",[14,6349,5506],{}," (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,6352,5514],{},[14,6354,5518],{}," so you find out in dev rather than in production.",[10,6357,6358,6359,4804,6362,6364],{},"One layout read up front, then cached values for the rest of the walk. The ",[14,6360,6361],{},"getBoundingClientRect()",[14,6363,5336],{}," calls happen in a single top-down pass with no intervening writes, no layout thrash: one forced reflow, then the captured snapshot is durable.",[94,6366,6368],{"id":6367},"the-15-named-primitives","The 15 named primitives",[10,6370,6371,6372,180,6375,180,6378,180,6381,180,6384,180,6387,180,6390,180,6393,180,6396,180,6399,180,6402,180,6405,180,6408,180,6411,180,6414,60],{},"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,6373,6374],{},"\u003CASkeletonCard>",[14,6376,6377],{},"\u003CASkeletonText>",[14,6379,6380],{},"\u003CASkeletonHeading>",[14,6382,6383],{},"\u003CASkeletonAvatar>",[14,6385,6386],{},"\u003CASkeletonImage>",[14,6388,6389],{},"\u003CASkeletonVideo>",[14,6391,6392],{},"\u003CASkeletonButton>",[14,6394,6395],{},"\u003CASkeletonInput>",[14,6397,6398],{},"\u003CASkeletonChip>",[14,6400,6401],{},"\u003CASkeletonListItem>",[14,6403,6404],{},"\u003CASkeletonTable>",[14,6406,6407],{},"\u003CASkeletonChart>",[14,6409,6410],{},"\u003CASkeletonForm>",[14,6412,6413],{},"\u003CASkeletonArticle>",[14,6415,6416],{},"\u003CASkeletonDivider>",[10,6418,6419],{},"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,6421,6422,6423,6426],{},"There's also ",[14,6424,6425],{},"\u003CASkeletonBlock>"," for the case where none of the variants fit and you want to compose your own:",[274,6428,6430],{"className":5177,"code":6429,"language":5179,"meta":279,"style":279},"\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,6431,6432,6457,6462,6467,6472,6477,6482,6490,6498],{"__ignoreMap":279},[283,6433,6434,6436,6438,6440,6442,6444,6446,6448,6450,6452,6455],{"class":285,"line":286},[283,6435,304],{"class":303},[283,6437,5189],{"class":5188},[283,6439,5192],{"class":289},[283,6441,533],{"class":303},[283,6443,5197],{"class":895},[283,6445,5382],{"class":303},[283,6447,5197],{"class":895},[283,6449,6068],{"class":299},[283,6451,533],{"class":303},[283,6453,6454],{"class":895},"\"flex items-start gap-4 p-4\"",[283,6456,5205],{"class":303},[283,6458,6459],{"class":285,"line":352},[283,6460,6461],{"class":303},"  \u003CASkeletonBlock type=\"circle\" :w=\"64\" :h=\"64\" \u002F>\n",[283,6463,6464],{"class":285,"line":372},[283,6465,6466],{"class":303},"  \u003Cdiv class=\"flex-1 space-y-2\">\n",[283,6468,6469],{"class":285,"line":411},[283,6470,6471],{"class":303},"    \u003CASkeletonBlock type=\"text\" :w=\"160\" :h=\"18\" \u002F>\n",[283,6473,6474],{"class":285,"line":437},[283,6475,6476],{"class":303},"    \u003CASkeletonBlock type=\"text\" :w=\"100\" :h=\"12\" \u002F>\n",[283,6478,6479],{"class":285,"line":539},[283,6480,6481],{"class":303},"    \u003CASkeletonBlock type=\"text\" :lines=\"3\" :h=\"14\" class=\"!mt-3\" \u002F>\n",[283,6483,6484,6486,6488],{"class":285,"line":546},[283,6485,6000],{"class":303},[283,6487,5189],{"class":5188},[283,6489,5205],{"class":303},[283,6491,6492,6494,6496],{"class":285,"line":591},[283,6493,5220],{"class":303},[283,6495,5189],{"class":5188},[283,6497,5205],{"class":303},[283,6499,6500],{"class":285,"line":616},[283,6501,6502],{"class":303},"\u003CUserCard v-else :data=\"user\" \u002F>\n",[10,6504,6505,6507,6508,6511,6512,6515],{},[14,6506,6425],{}," is flow-friendly. It composes with flex, grid, stack layouts. The ",[14,6509,6510],{},"lines"," prop on ",[14,6513,6514],{},"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.",[94,6517,6519],{"id":6518},"animations-and-theming-as-strategies","Animations and theming, as strategies",[10,6521,6522,6523,6526,6527,6530,6531,6534,6535,6538,6539,6542,6543,6546,6547,6550,6551,6554,6555,6558,6559,6562],{},"Four animation modes ship: ",[14,6524,6525],{},"shimmer"," (a gradient sweep on a ",[14,6528,6529],{},"::after"," pseudo-element, contained per block with ",[14,6532,6533],{},"overflow: hidden","), ",[14,6536,6537],{},"pulse"," (opacity cycling from 1 to ",[14,6540,6541],{},"--ak-skel-pulse-min"," and back), ",[14,6544,6545],{},"wave"," (a sliding gradient via ",[14,6548,6549],{},"background-position","), and ",[14,6552,6553],{},"none"," (static blocks). Each is a strategy, not a branch. You pass ",[14,6556,6557],{},"animation=\"pulse\""," and the right CSS class engages. No ",[14,6560,6561],{},"if (animation === 'pulse')"," chain in the component.",[10,6564,6565,6568],{},[14,6566,6567],{},"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,6570,6571,6572,6575,6576,6579],{},"Theming is CSS variables. The tokens are scoped under ",[14,6573,6574],{},"--ak-skel-*"," (with backward-compat aliases under ",[14,6577,6578],{},"--ak-skeleton-*"," for v1 consumers):",[4835,6581,6582,6592],{},[4838,6583,6584],{},[4841,6585,6586,6589],{},[4844,6587,6588],{},"Token",[4844,6590,6591],{},"Used for",[4854,6593,6594,6604,6614,6624,6634,6643,6653],{},[4841,6595,6596,6601],{},[4859,6597,6598],{},[14,6599,6600],{},"--ak-skel-base",[4859,6602,6603],{},"Block fill",[4841,6605,6606,6611],{},[4859,6607,6608],{},[14,6609,6610],{},"--ak-skel-highlight",[4859,6612,6613],{},"Shimmer \u002F wave sweep colour",[4841,6615,6616,6621],{},[4859,6617,6618],{},[14,6619,6620],{},"--ak-skel-radius",[4859,6622,6623],{},"Default block border radius",[4841,6625,6626,6631],{},[4859,6627,6628],{},[14,6629,6630],{},"--ak-skel-duration",[4859,6632,6633],{},"Animation cycle length",[4841,6635,6636,6640],{},[4859,6637,6638],{},[14,6639,6541],{},[4859,6641,6642],{},"Opacity at the trough of the pulse cycle",[4841,6644,6645,6650],{},[4859,6646,6647],{},[14,6648,6649],{},"--ak-skel-ring",[4859,6651,6652],{},"Subtle 1-px inset ring colour",[4841,6654,6655,6660],{},[4859,6656,6657],{},[14,6658,6659],{},"--ak-skel-icon",[4859,6661,6662],{},"Placeholder icon colour (image \u002F video variants)",[10,6664,6665,6666,6669],{},"Override them on ",[14,6667,6668],{},":root",", on a wrapper class for multi-tenant themes, or inline on a single instance:",[274,6671,6675],{"className":6672,"code":6673,"language":6674,"meta":279,"style":279},"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,6676,6677,6684,6713,6743,6758,6773],{"__ignoreMap":279},[283,6678,6679,6682],{"class":285,"line":286},[283,6680,6681],{"class":299},".tenant-acme",[283,6683,471],{"class":303},[283,6685,6686,6689,6692,6695,6697,6700,6703,6706,6709,6711],{"class":285,"line":352},[283,6687,6688],{"class":313},"  --ak-skel-base",[283,6690,6691],{"class":303},": ",[283,6693,6694],{"class":320},"hsl",[283,6696,463],{"class":303},[283,6698,6699],{"class":320},"220",[283,6701,6702],{"class":320}," 30",[283,6704,6705],{"class":289},"%",[283,6707,6708],{"class":320}," 18",[283,6710,6705],{"class":289},[283,6712,788],{"class":303},[283,6714,6715,6718,6720,6722,6724,6726,6729,6731,6733,6735,6738,6741],{"class":285,"line":372},[283,6716,6717],{"class":313},"  --ak-skel-highlight",[283,6719,6691],{"class":303},[283,6721,6694],{"class":320},[283,6723,463],{"class":303},[283,6725,6699],{"class":320},[283,6727,6728],{"class":320}," 60",[283,6730,6705],{"class":289},[283,6732,6728],{"class":320},[283,6734,6705],{"class":289},[283,6736,6737],{"class":303}," \u002F ",[283,6739,6740],{"class":320},"0.35",[283,6742,788],{"class":303},[283,6744,6745,6748,6750,6753,6756],{"class":285,"line":411},[283,6746,6747],{"class":313},"  --ak-skel-radius",[283,6749,6691],{"class":303},[283,6751,6752],{"class":320},"0.5",[283,6754,6755],{"class":289},"rem",[283,6757,483],{"class":303},[283,6759,6760,6763,6765,6768,6771],{"class":285,"line":437},[283,6761,6762],{"class":313},"  --ak-skel-duration",[283,6764,6691],{"class":303},[283,6766,6767],{"class":320},"2",[283,6769,6770],{"class":289},"s",[283,6772,483],{"class":303},[283,6774,6775],{"class":285,"line":539},[283,6776,440],{"class":303},[10,6778,6779,6780,6783,6784,6787,6788,6791,6792,6794,6795,6798],{},"Dark mode is automatic via ",[14,6781,6782],{},":where(.dark)"," selectors that retint the tokens (Tailwind's ",[14,6785,6786],{},".dark"," class, shadcn's, ",[14,6789,6790],{},"nuxt-color-mode","'s, anything that lands a ",[14,6793,6786],{}," ancestor), plus a ",[14,6796,6797],{},"@media (prefers-color-scheme: dark)"," fallback for the case where no explicit class is on the tree.",[10,6800,6801],{},"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.",[94,6803,6805],{"id":6804},"a11y-you-dont-have-to-configure","A11y you don't have to configure",[10,6807,6808,6809,6812,6813,6816,6817,6820,6821,6824],{},"The defaults are the ones you'd want anyway. Every wrapper and variant root carries ",[14,6810,6811],{},"role=\"status\""," while loading, ",[14,6814,6815],{},"aria-busy=\"true\""," mirroring the loading state, and ",[14,6818,6819],{},"aria-live=\"polite\""," so screen readers announce the loading state without interrupting the user. A visually-hidden ",[14,6822,6823],{},"\u003Cspan class=\"a-skel-sr-only\">Loading...\u003C\u002Fspan>"," ships with every variant so the announcement has text to read.",[10,6826,6827,6828,6831,6832,4804,6835,6838,6839,6842],{},"Every emitted shimmer surface carries ",[14,6829,6830],{},"aria-hidden=\"true\"",": the placeholder is decorative; the wrapper is what gets announced. Mirror-mode skeletons disable ",[14,6833,6834],{},"user-select",[14,6836,6837],{},"pointer-events"," on the slot tree so the placeholder can't be interacted with mid-load. ",[14,6840,6841],{},"prefers-reduced-motion"," strips animation.",[10,6844,6845,6846,6849],{},"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,6847,6848],{},"animation=\"none\""," is the lever) or doing something a11y-wise that I don't want to make easy.",[94,6851,6853],{"id":6852},"what-clone-mode-taught-me","What clone mode taught me",[10,6855,6856,6857,6860,6861,6864],{},"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,6858,6859],{},"border-radius: 0.75rem"," mixed with ",[14,6862,6863],{},"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,6866,6867,6868,6870],{},"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,6869,5336],{}," will tell you if you ask.",[10,6872,6873],{},"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,6875,6876],{},"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.",[5095,6878,6879],{},"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":279,"searchDepth":352,"depth":352,"links":6881},[6882,6883,6884,6885,6886,6887,6888,6889,6890],{"id":5165,"depth":352,"text":5166},{"id":5314,"depth":352,"text":5315},{"id":5531,"depth":352,"text":5532},{"id":5621,"depth":352,"text":5622},{"id":6273,"depth":352,"text":6274},{"id":6367,"depth":352,"text":6368},{"id":6518,"depth":352,"text":6519},{"id":6804,"depth":352,"text":6805},{"id":6852,"depth":352,"text":6853},"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، ومسار العمل الذي يلتقط ثم يُخزّن مؤقّتاً ثم يُعيد العرض خلفها.",[6895,6896,6897,6898,6899,6900,6292,5137,6901,6902],"Vue 3","Nuxt","skeleton loader","loading states","getComputedStyle","Range API","component library","accessibility",{},"\u002Fwrites\u002Finside-a-skeleton-self-generating-skeleton-loaders",{"title":5148,"description":6892},"writes\u002Finside-a-skeleton-self-generating-skeleton-loaders","داخل @alikhalilll\u002Fa-skeleton، مُحمِّلات skeleton تولّد نفسها بنفسها","VxGF-EgIL4tFkWPV2BLDCQVNC3i8p1quakbSMMxOdpw",{"id":6910,"title":6911,"body":6912,"date":11334,"description":11335,"description_ar":11336,"draft":5125,"extension":5126,"image":5127,"keywords":11337,"lang":5138,"meta":11345,"navigation":542,"path":11346,"seo":11347,"stem":11348,"title_ar":11349,"updatedAt":11334,"__hash__":11350},"writes\u002Fwrites\u002Fatell-input-the-library-rewrite.md","Notes from rewriting ATellInput as a published library",{"type":7,"value":6913,"toc":11324},[6914,6920,6923,6930,6933,6937,6940,6999,7013,7016,7032,7035,7122,7147,7287,7303,7314,7317,7321,7324,7412,7422,7440,7446,7960,7963,7981,8009,8024,8027,8030,8205,8222,8294,8297,8301,8308,8314,8638,8641,8707,8710,9018,9044,9050,9093,9104,9107,9190,9199,9203,9206,9213,9323,9341,9344,9348,9351,9354,9915,9957,9964,10103,10116,10119,10249,10256,10259,10391,10405,10408,10412,10439,10442,10674,10681,10905,10908,11079,11086,11089,11093,11099,11104,11226,11236,11239,11243,11254,11261,11264,11307,11310,11321],[10,6915,6916,6919],{},[14,6917,6918],{},"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,6921,6922],{},"This post is what happened when I tried to publish it.",[10,6924,6925,6926,6929],{},"The component now lives in ",[14,6927,6928],{},"@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,6931,6932],{},"I've grouped the changes around the seven that earned their place.",[94,6934,6936],{"id":6935},"two-models-two-shapes-number-out-iso2-inside","Two models, two shapes: number out, ISO2 inside",[10,6938,6939],{},"The old surface looked clean from a distance:",[274,6941,6943],{"className":276,"code":6942,"language":278,"meta":279,"style":279},"const phoneModelValue = defineModel\u003Cstring>('phone', { required: true });\nconst countryModelValue = defineModel\u003Cstring>('country', { required: true });\n",[14,6944,6945,6973],{"__ignoreMap":279},[283,6946,6947,6949,6952,6954,6957,6959,6961,6963,6966,6969,6971],{"class":285,"line":286},[283,6948,769],{"class":289},[283,6950,6951],{"class":320}," phoneModelValue",[283,6953,334],{"class":289},[283,6955,6956],{"class":299}," defineModel",[283,6958,304],{"class":303},[283,6960,499],{"class":320},[283,6962,310],{"class":303},[283,6964,6965],{"class":895},"'phone'",[283,6967,6968],{"class":303},", { required: ",[283,6970,976],{"class":320},[283,6972,979],{"class":303},[283,6974,6975,6977,6980,6982,6984,6986,6988,6990,6993,6995,6997],{"class":285,"line":352},[283,6976,769],{"class":289},[283,6978,6979],{"class":320}," countryModelValue",[283,6981,334],{"class":289},[283,6983,6956],{"class":299},[283,6985,304],{"class":303},[283,6987,499],{"class":320},[283,6989,310],{"class":303},[283,6991,6992],{"class":895},"'country'",[283,6994,6968],{"class":303},[283,6996,976],{"class":320},[283,6998,979],{"class":303},[10,7000,7001,7004,7005,7008,7009,7012],{},[14,7002,7003],{},"country"," was a string of dial digits: ",[14,7006,7007],{},"\"20\""," for Egypt, ",[14,7010,7011],{},"\"44\""," for the UK. Easy to serialize, easy to URL-encode, easy to put in a database column.",[10,7014,7015],{},"It also couldn't tell US from Canada.",[10,7017,7018,7021,7022,7025,7026,7028,7029,7031],{},[14,7019,7020],{},"+1"," is shared by 25+ NANP countries. If a Canadian user picked Canada in the dropdown and the v-model emitted ",[14,7023,7024],{},"\"1\"",", the parent had no way to know which ",[14,7027,7020],{}," was meant. Reload the page, hydrate from the URL, and you'd get back ",[14,7030,7024],{}," 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,7033,7034],{},"The new model splits it:",[274,7036,7038],{"className":276,"code":7037,"language":278,"meta":279,"style":279},"const phone = defineModel\u003Cstring>('phone', { default: '' });\nconst country = defineModel\u003Cnumber | null>('country', { default: null });\n\nconst selectedIso2 = ref\u003Cstring>('');\n",[14,7039,7040,7067,7097,7101],{"__ignoreMap":279},[283,7041,7042,7044,7047,7049,7051,7053,7055,7057,7059,7062,7065],{"class":285,"line":286},[283,7043,769],{"class":289},[283,7045,7046],{"class":320}," phone",[283,7048,334],{"class":289},[283,7050,6956],{"class":299},[283,7052,304],{"class":303},[283,7054,499],{"class":320},[283,7056,310],{"class":303},[283,7058,6965],{"class":895},[283,7060,7061],{"class":303},", { default: ",[283,7063,7064],{"class":895},"''",[283,7066,979],{"class":303},[283,7068,7069,7071,7074,7076,7078,7080,7083,7085,7087,7089,7091,7093,7095],{"class":285,"line":352},[283,7070,769],{"class":289},[283,7072,7073],{"class":320}," country",[283,7075,334],{"class":289},[283,7077,6956],{"class":299},[283,7079,304],{"class":303},[283,7081,7082],{"class":320},"number",[283,7084,848],{"class":289},[283,7086,2232],{"class":320},[283,7088,310],{"class":303},[283,7090,6992],{"class":895},[283,7092,7061],{"class":303},[283,7094,2809],{"class":320},[283,7096,979],{"class":303},[283,7098,7099],{"class":285,"line":372},[283,7100,543],{"emptyLinePlaceholder":542},[283,7102,7103,7105,7108,7110,7112,7114,7116,7118,7120],{"class":285,"line":411},[283,7104,769],{"class":289},[283,7106,7107],{"class":320}," selectedIso2",[283,7109,334],{"class":289},[283,7111,5724],{"class":299},[283,7113,304],{"class":303},[283,7115,499],{"class":320},[283,7117,310],{"class":303},[283,7119,7064],{"class":895},[283,7121,788],{"class":303},[10,7123,7124,7126,7127,180,7130,180,7132,7134,7135,7138,7139,7142,7143,7146],{},[14,7125,7003],{}," is a number now (",[14,7128,7129],{},"20",[14,7131,1525],{},[14,7133,2809],{},") because dial codes ",[1017,7136,7137],{},"are"," numbers, and ",[14,7140,7141],{},"Number"," survives URL\u002FDB\u002FJSON round-trips with no string-trimming weirdness. But the component also tracks an internal ",[14,7144,7145],{},"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:",[274,7148,7150],{"className":276,"code":7149,"language":278,"meta":279,"style":279},"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,7151,7152,7159,7164,7178,7192,7206,7212,7216,7237,7257,7269,7274,7283],{"__ignoreMap":279},[283,7153,7154,7157],{"class":285,"line":286},[283,7155,7156],{"class":299},"watch",[283,7158,2837],{"class":303},[283,7160,7161],{"class":285,"line":352},[283,7162,7163],{"class":303},"  country,\n",[283,7165,7166,7169,7172,7174,7176],{"class":285,"line":372},[283,7167,7168],{"class":303},"  (",[283,7170,7171],{"class":313},"next",[283,7173,1116],{"class":303},[283,7175,949],{"class":289},[283,7177,471],{"class":303},[283,7179,7180,7182,7185,7188,7190],{"class":285,"line":411},[283,7181,678],{"class":289},[283,7183,7184],{"class":303}," (next ",[283,7186,7187],{"class":289},"==",[283,7189,2232],{"class":320},[283,7191,1899],{"class":303},[283,7193,7194,7196,7199,7201,7204],{"class":285,"line":437},[283,7195,1943],{"class":289},[283,7197,7198],{"class":303}," (selectedIso2.value) selectedIso2.value ",[283,7200,533],{"class":289},[283,7202,7203],{"class":895}," ''",[283,7205,483],{"class":303},[283,7207,7208,7210],{"class":285,"line":539},[283,7209,1911],{"class":289},[283,7211,483],{"class":303},[283,7213,7214],{"class":285,"line":546},[283,7215,1988],{"class":303},[283,7217,7218,7220,7222,7225,7228,7230,7233,7235],{"class":285,"line":591},[283,7219,678],{"class":289},[283,7221,378],{"class":303},[283,7223,7224],{"class":299},"dialNumberFor",[283,7226,7227],{"class":303},"(selectedIso2.value) ",[283,7229,3935],{"class":289},[283,7231,7232],{"class":303}," next) ",[283,7234,867],{"class":289},[283,7236,483],{"class":303},[283,7238,7239,7241,7244,7246,7249,7251,7254],{"class":285,"line":616},[283,7240,594],{"class":289},[283,7242,7243],{"class":320}," iso2",[283,7245,334],{"class":289},[283,7247,7248],{"class":299}," resolveCountryIdentifier",[283,7250,463],{"class":303},[283,7252,7253],{"class":299},"String",[283,7255,7256],{"class":303},"(next));\n",[283,7258,7259,7261,7264,7266],{"class":285,"line":654},[283,7260,678],{"class":289},[283,7262,7263],{"class":303}," (iso2) selectedIso2.value ",[283,7265,533],{"class":289},[283,7267,7268],{"class":303}," iso2;\n",[283,7270,7271],{"class":285,"line":675},[283,7272,7273],{"class":303},"  },\n",[283,7275,7276,7279,7281],{"class":285,"line":703},[283,7277,7278],{"class":303},"  { immediate: ",[283,7280,976],{"class":320},[283,7282,4105],{"class":303},[283,7284,7285],{"class":285,"line":725},[283,7286,788],{"class":303},[10,7288,2580,7289,7292,7293,7296,7297,7299,7300,7302],{},[14,7290,7291],{},"if (dialNumberFor(selectedIso2.value) === next) return;"," line is the one I want to call out. When the parent writes ",[14,7294,7295],{},"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,7298,7145],{},".\nWithout that guard, the watcher would resolve ",[14,7301,7024],{}," to its alphabetically-first NANP country and silently flip Canada to whatever that is. The guard preserves the richer state.",[10,7304,7305,7306,7309,7310,7313],{},"The outward watch is ",[14,7307,7308],{},"flush: 'sync'"," so a separate ",[14,7311,7312],{},"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,7315,7316],{},"Two halves of the country, two distinct models, one source of truth for each.",[94,7318,7320],{"id":7319},"detection-first-the-picker-is-hidden-by-default","Detection-first: the picker is hidden by default",[10,7322,7323],{},"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:",[274,7325,7327],{"className":5177,"code":7326,"language":5179,"meta":279,"style":279},"\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,7328,7329,7336,7346,7356,7366,7375,7385,7394,7398,7403],{"__ignoreMap":279},[283,7330,7331,7333],{"class":285,"line":286},[283,7332,304],{"class":303},[283,7334,7335],{"class":5188},"Transition\n",[283,7337,7338,7341,7343],{"class":285,"line":352},[283,7339,7340],{"class":299},"  enter-active-class",[283,7342,533],{"class":303},[283,7344,7345],{"class":895},"\"transition-all duration-200 ease-out overflow-hidden\"\n",[283,7347,7348,7351,7353],{"class":285,"line":372},[283,7349,7350],{"class":299},"  leave-active-class",[283,7352,533],{"class":303},[283,7354,7355],{"class":895},"\"transition-all duration-150 ease-in overflow-hidden\"\n",[283,7357,7358,7361,7363],{"class":285,"line":411},[283,7359,7360],{"class":299},"  enter-from-class",[283,7362,533],{"class":303},[283,7364,7365],{"class":895},"\"opacity-0 -translate-x-1 max-w-0\"\n",[283,7367,7368,7371,7373],{"class":285,"line":437},[283,7369,7370],{"class":299},"  leave-to-class",[283,7372,533],{"class":303},[283,7374,7365],{"class":895},[283,7376,7377,7380,7382],{"class":285,"line":539},[283,7378,7379],{"class":299},"  enter-to-class",[283,7381,533],{"class":303},[283,7383,7384],{"class":895},"\"max-w-[12rem]\"\n",[283,7386,7387,7390,7392],{"class":285,"line":546},[283,7388,7389],{"class":299},"  leave-from-class",[283,7391,533],{"class":303},[283,7393,7384],{"class":895},[283,7395,7396],{"class":285,"line":591},[283,7397,5205],{"class":303},[283,7399,7400],{"class":285,"line":616},[283,7401,7402],{"class":303},"  \u003CACountrySelect v-if=\"!props.detectFromInput || selectedIso2\" ... \u002F>\n",[283,7404,7405,7407,7410],{"class":285,"line":654},[283,7406,5220],{"class":303},[283,7408,7409],{"class":5188},"Transition",[283,7411,5205],{"class":303},[10,7413,7414,7417,7418,7421],{},[14,7415,7416],{},"max-w"," collapses to zero when nothing is selected, animates open on the first successful match. ",[14,7419,7420],{},"detectFromInput=\"false\""," brings the always-visible picker back for callers who want the legacy shape.",[10,7423,7424,7425,7428,7429,7431,7432,7435,7436,7439],{},"The behaviour is opinionated. Most users in most countries ",[1017,7426,7427],{},"never"," type a ",[14,7430,4637],{}," prefix. They type the number they would type into their own phone: ",[14,7433,7434],{},"01066105963"," in Cairo, ",[14,7437,7438],{},"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,7441,7442,7443,317],{},"The \"detect what they're doing\" part is the only non-trivial piece. It runs through three tiers in ",[14,7444,7445],{},"matchLeadingDialCode",[274,7447,7449],{"className":276,"code":7448,"language":278,"meta":279,"style":279},"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,7450,7451,7480,7497,7501,7506,7513,7536,7548,7563,7570,7589,7593,7597,7606,7611,7615,7619,7624,7636,7659,7665,7685,7698,7718,7725,7742,7746,7750,7758,7763,7767,7771,7775,7780,7824,7844,7859,7878,7898,7924,7929,7934,7944,7948,7956],{"__ignoreMap":279},[283,7452,7453,7455,7458,7460,7463,7465,7467,7469,7471,7474,7476,7478],{"class":285,"line":286},[283,7454,816],{"class":289},[283,7456,7457],{"class":299}," matchLeadingDialCode",[283,7459,463],{"class":303},[283,7461,7462],{"class":313},"digits",[283,7464,317],{"class":289},[283,7466,321],{"class":320},[283,7468,841],{"class":303},[283,7470,317],{"class":289},[283,7472,7473],{"class":299}," DialMatch",[283,7475,848],{"class":289},[283,7477,2232],{"class":320},[283,7479,471],{"class":303},[283,7481,7482,7484,7486,7488,7491,7493,7495],{"class":285,"line":352},[283,7483,375],{"class":289},[283,7485,378],{"class":303},[283,7487,381],{"class":289},[283,7489,7490],{"class":303},"digits) ",[283,7492,867],{"class":289},[283,7494,2232],{"class":320},[283,7496,483],{"class":303},[283,7498,7499],{"class":285,"line":372},[283,7500,543],{"emptyLinePlaceholder":542},[283,7502,7503],{"class":285,"line":411},[283,7504,7505],{"class":917},"  \u002F\u002F Tier 1: international parse, disambiguates NANP and strips the calling code.\n",[283,7507,7508,7511],{"class":285,"line":437},[283,7509,7510],{"class":289},"  try",[283,7512,471],{"class":303},[283,7514,7515,7517,7520,7522,7525,7527,7530,7532,7534],{"class":285,"line":539},[283,7516,594],{"class":289},[283,7518,7519],{"class":320}," parsed",[283,7521,334],{"class":289},[283,7523,7524],{"class":299}," parsePhoneNumberFromString",[283,7526,463],{"class":303},[283,7528,7529],{"class":895},"`+${",[283,7531,7462],{"class":303},[283,7533,1692],{"class":895},[283,7535,788],{"class":303},[283,7537,7538,7540,7543,7545],{"class":285,"line":546},[283,7539,678],{"class":289},[283,7541,7542],{"class":303}," (parsed?.country ",[283,7544,2770],{"class":289},[283,7546,7547],{"class":303}," parsed.countryCallingCode) {\n",[283,7549,7550,7552,7555,7557,7560],{"class":285,"line":591},[283,7551,2994],{"class":289},[283,7553,7554],{"class":320}," parsedCountry",[283,7556,334],{"class":289},[283,7558,7559],{"class":299}," getCountryByValue",[283,7561,7562],{"class":303},"(parsed.country);\n",[283,7564,7565,7567],{"class":285,"line":616},[283,7566,1943],{"class":289},[283,7568,7569],{"class":303}," (parsedCountry) {\n",[283,7571,7572,7574,7577,7579,7582,7584,7586],{"class":285,"line":654},[283,7573,3962],{"class":289},[283,7575,7576],{"class":303}," { country: parsedCountry, nationalNumber: ",[283,7578,7253],{"class":299},[283,7580,7581],{"class":303},"(parsed.nationalNumber ",[283,7583,1305],{"class":289},[283,7585,7203],{"class":895},[283,7587,7588],{"class":303},") };\n",[283,7590,7591],{"class":285,"line":675},[283,7592,3566],{"class":303},[283,7594,7595],{"class":285,"line":703},[283,7596,1988],{"class":303},[283,7598,7599,7602,7604],{"class":285,"line":725},[283,7600,7601],{"class":303},"  } ",[283,7603,1925],{"class":289},[283,7605,471],{"class":303},[283,7607,7608],{"class":285,"line":731},[283,7609,7610],{"class":917},"    \u002F* libphonenumber throws on partial input, fall through *\u002F\n",[283,7612,7613],{"class":285,"line":1377},[283,7614,1453],{"class":303},[283,7616,7617],{"class":285,"line":1402},[283,7618,543],{"emptyLinePlaceholder":542},[283,7620,7621],{"class":285,"line":1429},[283,7622,7623],{"class":917},"  \u002F\u002F Tier 2: national-format parse using the silently-inferred country as a hint.\n",[283,7625,7626,7628,7631,7633],{"class":285,"line":1450},[283,7627,355],{"class":289},[283,7629,7630],{"class":320}," hint",[283,7632,334],{"class":289},[283,7634,7635],{"class":303}," inferredCountry.value;\n",[283,7637,7638,7640,7643,7645,7648,7651,7654,7657],{"class":285,"line":1456},[283,7639,375],{"class":289},[283,7641,7642],{"class":303}," (hint ",[283,7644,2770],{"class":289},[283,7646,7647],{"class":303}," digits.",[283,7649,7650],{"class":320},"length",[283,7652,7653],{"class":289}," >=",[283,7655,7656],{"class":320}," 4",[283,7658,1899],{"class":303},[283,7660,7661,7663],{"class":285,"line":1461},[283,7662,1904],{"class":289},[283,7664,471],{"class":303},[283,7666,7667,7669,7671,7673,7675,7678,7680,7683],{"class":285,"line":1487},[283,7668,2994],{"class":289},[283,7670,7519],{"class":320},[283,7672,334],{"class":289},[283,7674,7524],{"class":299},[283,7676,7677],{"class":303},"(digits, hint ",[283,7679,425],{"class":289},[283,7681,7682],{"class":299}," CountryCode",[283,7684,788],{"class":303},[283,7686,7687,7689,7692,7695],{"class":285,"line":1499},[283,7688,1943],{"class":289},[283,7690,7691],{"class":303}," (parsed?.",[283,7693,7694],{"class":299},"isValid",[283,7696,7697],{"class":303},"()) {\n",[283,7699,7700,7703,7706,7708,7710,7713,7715],{"class":285,"line":1530},[283,7701,7702],{"class":289},"        const",[283,7704,7705],{"class":320}," matched",[283,7707,334],{"class":289},[283,7709,7559],{"class":299},[283,7711,7712],{"class":303},"(parsed.country ",[283,7714,2229],{"class":289},[283,7716,7717],{"class":303}," hint);\n",[283,7719,7720,7722],{"class":285,"line":1535},[283,7721,4218],{"class":289},[283,7723,7724],{"class":303}," (matched) {\n",[283,7726,7727,7729,7732,7734,7736,7738,7740],{"class":285,"line":1559},[283,7728,4249],{"class":289},[283,7730,7731],{"class":303}," { country: matched, nationalNumber: ",[283,7733,7253],{"class":299},[283,7735,7581],{"class":303},[283,7737,1305],{"class":289},[283,7739,7203],{"class":895},[283,7741,7588],{"class":303},[283,7743,7744],{"class":285,"line":1569},[283,7745,4110],{"class":303},[283,7747,7748],{"class":285,"line":1596},[283,7749,3566],{"class":303},[283,7751,7752,7754,7756],{"class":285,"line":1601},[283,7753,1922],{"class":303},[283,7755,1925],{"class":289},[283,7757,471],{"class":303},[283,7759,7760],{"class":285,"line":1606},[283,7761,7762],{"class":917},"      \u002F* fall through *\u002F\n",[283,7764,7765],{"class":285,"line":1614},[283,7766,1988],{"class":303},[283,7768,7769],{"class":285,"line":4187},[283,7770,1453],{"class":303},[283,7772,7773],{"class":285,"line":4207},[283,7774,543],{"emptyLinePlaceholder":542},[283,7776,7777],{"class":285,"line":4215},[283,7778,7779],{"class":917},"  \u002F\u002F Tier 3: longest-prefix match over our own dial-digits index.\n",[283,7781,7782,7784,7786,7788,7791,7793,7796,7799,7801,7804,7807,7809,7812,7814,7816,7819,7822],{"class":285,"line":4224},[283,7783,1871],{"class":289},[283,7785,378],{"class":303},[283,7787,1876],{"class":289},[283,7789,7790],{"class":303}," len ",[283,7792,533],{"class":289},[283,7794,7795],{"class":303}," Math.",[283,7797,7798],{"class":299},"min",[283,7800,463],{"class":303},[283,7802,7803],{"class":320},"3",[283,7805,7806],{"class":303},", digits.",[283,7808,7650],{"class":320},[283,7810,7811],{"class":303},"); len ",[283,7813,3050],{"class":289},[283,7815,4727],{"class":320},[283,7817,7818],{"class":303},"; len",[283,7820,7821],{"class":289},"--",[283,7823,1899],{"class":303},[283,7825,7826,7828,7831,7833,7835,7837,7839,7841],{"class":285,"line":4246},[283,7827,594],{"class":289},[283,7829,7830],{"class":320}," prefix",[283,7832,334],{"class":289},[283,7834,7647],{"class":303},[283,7836,4695],{"class":299},[283,7838,463],{"class":303},[283,7840,2680],{"class":320},[283,7842,7843],{"class":303},", len);\n",[283,7845,7846,7848,7851,7853,7856],{"class":285,"line":4254},[283,7847,594],{"class":289},[283,7849,7850],{"class":320}," group",[283,7852,334],{"class":289},[283,7854,7855],{"class":299}," getCountriesByDial",[283,7857,7858],{"class":303},"(prefix);\n",[283,7860,7861,7863,7865,7867,7870,7872,7874,7876],{"class":285,"line":4259},[283,7862,678],{"class":289},[283,7864,378],{"class":303},[283,7866,381],{"class":289},[283,7868,7869],{"class":303},"group.",[283,7871,7650],{"class":320},[283,7873,1116],{"class":303},[283,7875,4679],{"class":289},[283,7877,483],{"class":303},[283,7879,7880,7882,7885,7887,7889,7891,7894,7896],{"class":285,"line":4277},[283,7881,594],{"class":289},[283,7883,7884],{"class":320}," nationalNumber",[283,7886,334],{"class":289},[283,7888,7647],{"class":303},[283,7890,4695],{"class":299},[283,7892,7893],{"class":303},"(prefix.",[283,7895,7650],{"class":320},[283,7897,788],{"class":303},[283,7899,7900,7902,7905,7907,7910,7912,7914,7916,7919,7921],{"class":285,"line":4282},[283,7901,678],{"class":289},[283,7903,7904],{"class":303}," (group.",[283,7906,7650],{"class":320},[283,7908,7909],{"class":289}," ===",[283,7911,4727],{"class":320},[283,7913,1116],{"class":303},[283,7915,867],{"class":289},[283,7917,7918],{"class":303}," { country: group[",[283,7920,2680],{"class":320},[283,7922,7923],{"class":303},"], nationalNumber };\n",[283,7925,7926],{"class":285,"line":4287},[283,7927,7928],{"class":917},"    \u002F\u002F ambiguity: prefer current selection, then recents, then first\n",[283,7930,7931],{"class":285,"line":4302},[283,7932,7933],{"class":917},"    \u002F\u002F (see \"Recents as the tiebreaker\" below)\n",[283,7935,7936,7938,7940,7942],{"class":285,"line":4320},[283,7937,706],{"class":289},[283,7939,7918],{"class":303},[283,7941,2680],{"class":320},[283,7943,7923],{"class":303},[283,7945,7946],{"class":285,"line":4345},[283,7947,1453],{"class":303},[283,7949,7950,7952,7954],{"class":285,"line":4351},[283,7951,414],{"class":289},[283,7953,2232],{"class":320},[283,7955,483],{"class":303},[283,7957,7958],{"class":285,"line":4357},[283,7959,440],{"class":303},[10,7961,7962],{},"The tiers exist because each handles a class of input the other tiers can't.",[10,7964,7965,7966,7968,7969,7972,7973,7976,7977,7980],{},"Tier 1 takes whatever the user typed, prepends ",[14,7967,4637],{},", and asks libphonenumber to parse it as an international number. If the user typed ",[14,7970,7971],{},"447911123456",", this matches the UK. If they typed ",[14,7974,7975],{},"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,7978,7979],{},"447"," isn't a complete number; parsing it raises, and we fall through.",[10,7982,7983,7984,7986,7987,7990,7991,7994,7995,7997,7998,8000,8001,8004,8005,8008],{},"Tier 2 is the path for users who type the number they actually use locally. ",[14,7985,7434],{}," doesn't start with a dial code, so Tier 1 can't help. But if we ",[1017,7988,7989],{},"silently"," know the user is probably in Egypt (from IP or timezone; more on that next), we can pass ",[14,7992,7993],{},"\"EG\""," as a parsing hint and libphonenumber will recognise ",[14,7996,7434],{}," as a valid Egyptian mobile number, strip the leading ",[14,7999,2680],{}," (Egypt's national prefix), and hand back the canonical national significant number ",[14,8002,8003],{},"1066105963",". The ",[14,8006,8007],{},"digits.length >= 4"," guard prevents the parser from treating two-digit area-code starts as full numbers.",[10,8010,8011,8012,8015,8016,8019,8020,8023],{},"Tier 3 is the fallback for \"you typed ",[14,8013,8014],{},"44"," and nothing else.\" Neither libphonenumber pass can match; the input is too short. But our own dial-digits index can: ",[14,8017,8018],{},"byDialDigits.get('44')"," returns the UK group, length 1, done. The bucketed-",[14,8021,8022],{},"Map\u003Cstring, CountryOption[]>"," shape from the old version earns its keep here.",[10,8025,8026],{},"Three tiers feels like a lot. It's the minimum: each tier handles inputs the other two reject.",[10,8028,8029],{},"The handler that calls into it stays small:",[274,8031,8033],{"className":276,"code":8032,"language":278,"meta":279,"style":279},"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,8034,8035,8049,8058,8073,8089,8101,8116,8130,8145,8156,8166,8176,8180,8201],{"__ignoreMap":279},[283,8036,8037,8039,8042,8044,8047],{"class":285,"line":286},[283,8038,769],{"class":289},[283,8040,8041],{"class":320}," detectAndApply",[283,8043,334],{"class":289},[283,8045,8046],{"class":299}," useDebounceFn",[283,8048,2837],{"class":303},[283,8050,8051,8054,8056],{"class":285,"line":352},[283,8052,8053],{"class":303},"  () ",[283,8055,949],{"class":289},[283,8057,471],{"class":303},[283,8059,8060,8062,8064,8066,8069,8071],{"class":285,"line":372},[283,8061,678],{"class":289},[283,8063,378],{"class":303},[283,8065,381],{"class":289},[283,8067,8068],{"class":303},"props.detectFromInput) ",[283,8070,867],{"class":289},[283,8072,483],{"class":303},[283,8074,8075,8077,8080,8082,8085,8087],{"class":285,"line":411},[283,8076,678],{"class":289},[283,8078,8079],{"class":303}," (userPickedCountry.value ",[283,8081,2229],{"class":289},[283,8083,8084],{"class":303}," selectedIso2.value) ",[283,8086,867],{"class":289},[283,8088,483],{"class":303},[283,8090,8091,8093,8096,8098],{"class":285,"line":437},[283,8092,594],{"class":289},[283,8094,8095],{"class":320}," current",[283,8097,334],{"class":289},[283,8099,8100],{"class":303}," phone.value;\n",[283,8102,8103,8105,8107,8109,8112,8114],{"class":285,"line":539},[283,8104,678],{"class":289},[283,8106,378],{"class":303},[283,8108,381],{"class":289},[283,8110,8111],{"class":303},"current) ",[283,8113,867],{"class":289},[283,8115,483],{"class":303},[283,8117,8118,8120,8123,8125,8127],{"class":285,"line":546},[283,8119,594],{"class":289},[283,8121,8122],{"class":320}," match",[283,8124,334],{"class":289},[283,8126,7457],{"class":299},[283,8128,8129],{"class":303},"(current);\n",[283,8131,8132,8134,8136,8138,8141,8143],{"class":285,"line":591},[283,8133,678],{"class":289},[283,8135,378],{"class":303},[283,8137,381],{"class":289},[283,8139,8140],{"class":303},"match) ",[283,8142,867],{"class":289},[283,8144,483],{"class":303},[283,8146,8147,8150,8152,8154],{"class":285,"line":616},[283,8148,8149],{"class":303},"    autoSettingCountry.value ",[283,8151,533],{"class":289},[283,8153,4031],{"class":320},[283,8155,483],{"class":303},[283,8157,8158,8161,8163],{"class":285,"line":654},[283,8159,8160],{"class":303},"    selectedIso2.value ",[283,8162,533],{"class":289},[283,8164,8165],{"class":303}," match.country.value;\n",[283,8167,8168,8171,8173],{"class":285,"line":675},[283,8169,8170],{"class":303},"    phone.value ",[283,8172,533],{"class":289},[283,8174,8175],{"class":303}," match.nationalNumber;\n",[283,8177,8178],{"class":285,"line":703},[283,8179,7273],{"class":303},[283,8181,8182,8185,8187,8189,8191,8194,8196,8198],{"class":285,"line":725},[283,8183,8184],{"class":299},"  computed",[283,8186,5745],{"class":303},[283,8188,949],{"class":289},[283,8190,7795],{"class":303},[283,8192,8193],{"class":299},"max",[283,8195,463],{"class":303},[283,8197,2680],{"class":320},[283,8199,8200],{"class":303},", props.detectDebounceMs))\n",[283,8202,8203],{"class":285,"line":731},[283,8204,788],{"class":303},[10,8206,8207,6301,8210,8213,8214,8217,8218,8221],{},[14,8208,8209],{},"useDebounceFn",[14,8211,8212],{},"phone.value"," at the time the timer fires, not at schedule time, so a burst of keystrokes collapses into one parse. ",[14,8215,8216],{},"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 ",[1017,8219,8220],{},"not"," debounced; it reverts the picker to hidden immediately:",[274,8223,8225],{"className":276,"code":8224,"language":278,"meta":279,"style":279},"if (!cleaned) {\n  autoSettingCountry.value = true;\n  selectedIso2.value = '';\n  phone.value = '';\n  userPickedCountry.value = false;\n  return;\n}\n",[14,8226,8227,8239,8250,8261,8272,8284,8290],{"__ignoreMap":279},[283,8228,8229,8232,8234,8236],{"class":285,"line":286},[283,8230,8231],{"class":289},"if",[283,8233,378],{"class":303},[283,8235,381],{"class":289},[283,8237,8238],{"class":303},"cleaned) {\n",[283,8240,8241,8244,8246,8248],{"class":285,"line":352},[283,8242,8243],{"class":303},"  autoSettingCountry.value ",[283,8245,533],{"class":289},[283,8247,4031],{"class":320},[283,8249,483],{"class":303},[283,8251,8252,8255,8257,8259],{"class":285,"line":372},[283,8253,8254],{"class":303},"  selectedIso2.value ",[283,8256,533],{"class":289},[283,8258,7203],{"class":895},[283,8260,483],{"class":303},[283,8262,8263,8266,8268,8270],{"class":285,"line":411},[283,8264,8265],{"class":303},"  phone.value ",[283,8267,533],{"class":289},[283,8269,7203],{"class":895},[283,8271,483],{"class":303},[283,8273,8274,8277,8279,8282],{"class":285,"line":437},[283,8275,8276],{"class":303},"  userPickedCountry.value ",[283,8278,533],{"class":289},[283,8280,8281],{"class":320}," false",[283,8283,483],{"class":303},[283,8285,8286,8288],{"class":285,"line":539},[283,8287,414],{"class":289},[283,8289,483],{"class":303},[283,8291,8292],{"class":285,"line":546},[283,8293,440],{"class":303},[10,8295,8296],{},"Debouncing the clear would mean the dropdown lingers for 150ms after the input is empty, which looks wrong every time.",[94,8298,8300],{"id":8299},"the-environment-detection-chain-and-why-its-pluggable","The environment-detection chain (and why it's pluggable)",[10,8302,8303,8304,8307],{},"Tier 2 above depends on ",[14,8305,8306],{},"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,8309,8310,8311,317],{},"The new chain is ",[14,8312,8313],{},"detectCountry()",[274,8315,8317],{"className":276,"code":8316,"language":278,"meta":279,"style":279},"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,8318,8319,8354,8360,8372,8384,8396,8408,8419,8428,8432,8439,8453,8465,8469,8473,8487,8498,8502,8506,8518,8535,8542,8555,8562,8566,8570,8574,8595,8616,8627,8634],{"__ignoreMap":279},[283,8320,8321,8323,8325,8327,8330,8332,8335,8337,8340,8342,8344,8346,8348,8350,8352],{"class":285,"line":286},[283,8322,290],{"class":289},[283,8324,293],{"class":289},[283,8326,296],{"class":289},[283,8328,8329],{"class":299}," detectCountry",[283,8331,463],{"class":303},[283,8333,8334],{"class":313},"opts",[283,8336,317],{"class":289},[283,8338,8339],{"class":299}," DetectCountryOptions",[283,8341,334],{"class":289},[283,8343,337],{"class":303},[283,8345,317],{"class":289},[283,8347,342],{"class":299},[283,8349,304],{"class":303},[283,8351,499],{"class":320},[283,8353,349],{"class":303},[283,8355,8356,8358],{"class":285,"line":352},[283,8357,355],{"class":289},[283,8359,471],{"class":303},[283,8361,8362,8365,8367,8370],{"class":285,"line":372},[283,8363,8364],{"class":320},"    strategy",[283,8366,334],{"class":289},[283,8368,8369],{"class":895}," 'auto'",[283,8371,2048],{"class":303},[283,8373,8374,8377,8379,8382],{"class":285,"line":411},[283,8375,8376],{"class":320},"    ipEndpoint",[283,8378,334],{"class":289},[283,8380,8381],{"class":895}," 'https:\u002F\u002Fipapi.co\u002Fjson\u002F'",[283,8383,2048],{"class":303},[283,8385,8386,8389,8391,8394],{"class":285,"line":437},[283,8387,8388],{"class":320},"    defaultCountry",[283,8390,334],{"class":289},[283,8392,8393],{"class":895}," 'US'",[283,8395,2048],{"class":303},[283,8397,8398,8401,8403,8406],{"class":285,"line":539},[283,8399,8400],{"class":320},"    timeoutMs",[283,8402,334],{"class":289},[283,8404,8405],{"class":320}," 2000",[283,8407,2048],{"class":303},[283,8409,8410,8413,8415,8417],{"class":285,"line":546},[283,8411,8412],{"class":320},"    cache",[283,8414,334],{"class":289},[283,8416,4031],{"class":320},[283,8418,2048],{"class":303},[283,8420,8421,8423,8425],{"class":285,"line":591},[283,8422,7601],{"class":303},[283,8424,533],{"class":289},[283,8426,8427],{"class":303}," opts;\n",[283,8429,8430],{"class":285,"line":616},[283,8431,543],{"emptyLinePlaceholder":542},[283,8433,8434,8436],{"class":285,"line":654},[283,8435,375],{"class":289},[283,8437,8438],{"class":303}," (cache) {\n",[283,8440,8441,8443,8446,8448,8451],{"class":285,"line":675},[283,8442,594],{"class":289},[283,8444,8445],{"class":320}," cached",[283,8447,334],{"class":289},[283,8449,8450],{"class":299}," readCache",[283,8452,613],{"class":303},[283,8454,8455,8457,8460,8462],{"class":285,"line":703},[283,8456,678],{"class":289},[283,8458,8459],{"class":303}," (cached) ",[283,8461,867],{"class":289},[283,8463,8464],{"class":303}," cached;\n",[283,8466,8467],{"class":285,"line":725},[283,8468,1453],{"class":303},[283,8470,8471],{"class":285,"line":731},[283,8472,543],{"emptyLinePlaceholder":542},[283,8474,8475,8477,8480,8482,8485],{"class":285,"line":1377},[283,8476,375],{"class":289},[283,8478,8479],{"class":303}," (strategy ",[283,8481,3935],{"class":289},[283,8483,8484],{"class":895}," 'none'",[283,8486,1899],{"class":303},[283,8488,8489,8491,8494,8496],{"class":285,"line":1402},[283,8490,706],{"class":289},[283,8492,8493],{"class":303}," defaultCountry.",[283,8495,3801],{"class":299},[283,8497,613],{"class":303},[283,8499,8500],{"class":285,"line":1429},[283,8501,1453],{"class":303},[283,8503,8504],{"class":285,"line":1450},[283,8505,543],{"emptyLinePlaceholder":542},[283,8507,8508,8510,8512,8514,8516],{"class":285,"line":1456},[283,8509,375],{"class":289},[283,8511,8479],{"class":303},[283,8513,3935],{"class":289},[283,8515,8369],{"class":895},[283,8517,1899],{"class":303},[283,8519,8520,8522,8525,8527,8529,8532],{"class":285,"line":1461},[283,8521,594],{"class":289},[283,8523,8524],{"class":320}," ipResult",[283,8526,334],{"class":289},[283,8528,363],{"class":289},[283,8530,8531],{"class":299}," tryIp",[283,8533,8534],{"class":303},"(ipEndpoint, timeoutMs);\n",[283,8536,8537,8539],{"class":285,"line":1487},[283,8538,678],{"class":289},[283,8540,8541],{"class":303}," (ipResult) {\n",[283,8543,8544,8546,8549,8552],{"class":285,"line":1499},[283,8545,1943],{"class":289},[283,8547,8548],{"class":303}," (cache) ",[283,8550,8551],{"class":299},"writeCache",[283,8553,8554],{"class":303},"(ipResult);\n",[283,8556,8557,8559],{"class":285,"line":1530},[283,8558,1911],{"class":289},[283,8560,8561],{"class":303}," ipResult;\n",[283,8563,8564],{"class":285,"line":1535},[283,8565,1988],{"class":303},[283,8567,8568],{"class":285,"line":1559},[283,8569,1453],{"class":303},[283,8571,8572],{"class":285,"line":1569},[283,8573,543],{"emptyLinePlaceholder":542},[283,8575,8576,8578,8581,8583,8586,8588,8590,8593],{"class":285,"line":1596},[283,8577,355],{"class":289},[283,8579,8580],{"class":320}," localResult",[283,8582,334],{"class":289},[283,8584,8585],{"class":299}," tryTimezone",[283,8587,422],{"class":303},[283,8589,1305],{"class":289},[283,8591,8592],{"class":299}," tryLocale",[283,8594,613],{"class":303},[283,8596,8597,8599,8602,8604,8607,8609,8612,8614],{"class":285,"line":1601},[283,8598,355],{"class":289},[283,8600,8601],{"class":320}," final",[283,8603,334],{"class":289},[283,8605,8606],{"class":303}," (localResult ",[283,8608,1305],{"class":289},[283,8610,8611],{"class":303}," defaultCountry).",[283,8613,3801],{"class":299},[283,8615,613],{"class":303},[283,8617,8618,8620,8622,8624],{"class":285,"line":1606},[283,8619,375],{"class":289},[283,8621,8548],{"class":303},[283,8623,8551],{"class":299},[283,8625,8626],{"class":303},"(final);\n",[283,8628,8629,8631],{"class":285,"line":1614},[283,8630,414],{"class":289},[283,8632,8633],{"class":303}," final;\n",[283,8635,8636],{"class":285,"line":4187},[283,8637,440],{"class":303},[10,8639,8640],{},"Four signals, evaluated in cost order: cheap-and-cached first, expensive-and-networked last.",[65,8642,8643,8649,8660,8687,8700],{},[68,8644,8645,8648],{},[14,8646,8647],{},"sessionStorage"," cache. The user already resolved to a country during this session; we don't redo the work.",[68,8650,8651,8652,8655,8656,8659],{},"IP geolocation via ",[14,8653,8654],{},"ipapi.co"," (configurable). The most accurate signal but it costs a network call and a tiny bit of privacy. ",[14,8657,8658],{},"strategy: 'locale'"," skips this step for consumers who don't want the request.",[68,8661,8662,8663,8666,8667,8670,8671,180,8674,8670,8677,180,8680,8670,8683,8686],{},"Timezone, via ",[14,8664,8665],{},"Intl.DateTimeFormat().resolvedOptions().timeZone",", looked up against a hand-rolled table that maps ",[14,8668,8669],{},"Africa\u002FCairo"," to ",[14,8672,8673],{},"EG",[14,8675,8676],{},"Asia\u002FRiyadh",[14,8678,8679],{},"SA",[14,8681,8682],{},"Europe\u002FLondon",[14,8684,8685],{},"GB",", and so on. The map covers the most-populated zones; it's honest about not covering every one. A miss falls through.",[68,8688,8689,8692,8693,254,8696,8699],{},[14,8690,8691],{},"navigator.language",", which gives ",[14,8694,8695],{},"en-EG",[14,8697,8698],{},"ar-SA"," and similar. The region suffix is the country. Less reliable (users move; browser locales don't), but free.",[68,8701,8702,8703,8706],{},"The caller-supplied default. ",[14,8704,8705],{},"defaultCountry: 'US'"," is the library default; consumers can pick their own.",[10,8708,8709],{},"The IP step is the one that needed real care, because the call can hang:",[274,8711,8713],{"className":276,"code":8712,"language":278,"meta":279,"style":279},"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,8714,8715,8757,8791,8805,8827,8833,8853,8869,8910,8941,8977,8985,8993,9002,9010,9014],{"__ignoreMap":279},[283,8716,8717,8719,8721,8723,8725,8728,8730,8732,8734,8737,8739,8741,8743,8745,8747,8749,8751,8753,8755],{"class":285,"line":286},[283,8718,1719],{"class":289},[283,8720,296],{"class":289},[283,8722,8531],{"class":299},[283,8724,463],{"class":303},[283,8726,8727],{"class":313},"endpoint",[283,8729,317],{"class":289},[283,8731,321],{"class":320},[283,8733,180],{"class":303},[283,8735,8736],{"class":313},"timeoutMs",[283,8738,317],{"class":289},[283,8740,2632],{"class":320},[283,8742,841],{"class":303},[283,8744,317],{"class":289},[283,8746,342],{"class":299},[283,8748,304],{"class":303},[283,8750,499],{"class":320},[283,8752,848],{"class":289},[283,8754,2232],{"class":320},[283,8756,349],{"class":303},[283,8758,8759,8761,8763,8765,8768,8770,8772,8775,8778,8780,8783,8785,8787,8789],{"class":285,"line":352},[283,8760,375],{"class":289},[283,8762,378],{"class":303},[283,8764,381],{"class":289},[283,8766,8767],{"class":299},"isBrowser",[283,8769,422],{"class":303},[283,8771,2229],{"class":289},[283,8773,8774],{"class":289}," typeof",[283,8776,8777],{"class":303}," fetch ",[283,8779,1739],{"class":289},[283,8781,8782],{"class":895}," 'function'",[283,8784,1116],{"class":303},[283,8786,867],{"class":289},[283,8788,2232],{"class":320},[283,8790,483],{"class":303},[283,8792,8793,8795,8797,8799,8801,8803],{"class":285,"line":372},[283,8794,355],{"class":289},[283,8796,925],{"class":320},[283,8798,334],{"class":289},[283,8800,390],{"class":289},[283,8802,932],{"class":299},[283,8804,613],{"class":303},[283,8806,8807,8809,8812,8814,8816,8818,8820,8822,8824],{"class":285,"line":411},[283,8808,355],{"class":289},[283,8810,8811],{"class":320}," timer",[283,8813,334],{"class":289},[283,8815,1969],{"class":299},[283,8817,5745],{"class":303},[283,8819,949],{"class":289},[283,8821,952],{"class":303},[283,8823,955],{"class":299},[283,8825,8826],{"class":303},"(), timeoutMs);\n",[283,8828,8829,8831],{"class":285,"line":437},[283,8830,7510],{"class":289},[283,8832,471],{"class":303},[283,8834,8835,8837,8839,8841,8843,8845,8848,8851],{"class":285,"line":539},[283,8836,594],{"class":289},[283,8838,358],{"class":320},[283,8840,334],{"class":289},[283,8842,363],{"class":289},[283,8844,366],{"class":299},[283,8846,8847],{"class":303},"(endpoint, { signal: controller.signal, credentials: ",[283,8849,8850],{"class":895},"'omit'",[283,8852,979],{"class":303},[283,8854,8855,8857,8859,8861,8863,8865,8867],{"class":285,"line":546},[283,8856,678],{"class":289},[283,8858,378],{"class":303},[283,8860,381],{"class":289},[283,8862,384],{"class":303},[283,8864,867],{"class":289},[283,8866,2232],{"class":320},[283,8868,483],{"class":303},[283,8870,8871,8873,8876,8878,8880,8882,8884,8886,8889,8891,8893,8896,8898,8900,8902,8904,8906,8908],{"class":285,"line":591},[283,8872,594],{"class":289},[283,8874,8875],{"class":320}," data",[283,8877,334],{"class":289},[283,8879,378],{"class":303},[283,8881,399],{"class":289},[283,8883,402],{"class":303},[283,8885,419],{"class":299},[283,8887,8888],{"class":303},"()) ",[283,8890,425],{"class":289},[283,8892,517],{"class":303},[283,8894,8895],{"class":313},"country_code",[283,8897,491],{"class":289},[283,8899,321],{"class":320},[283,8901,1069],{"class":303},[283,8903,7003],{"class":313},[283,8905,491],{"class":289},[283,8907,321],{"class":320},[283,8909,1695],{"class":303},[283,8911,8912,8914,8917,8919,8922,8924,8927,8929,8931,8933,8935,8937,8939],{"class":285,"line":616},[283,8913,594],{"class":289},[283,8915,8916],{"class":320}," code",[283,8918,334],{"class":289},[283,8920,8921],{"class":303}," (data.country_code ",[283,8923,1305],{"class":289},[283,8925,8926],{"class":303}," data.country ",[283,8928,1305],{"class":289},[283,8930,7203],{"class":895},[283,8932,3798],{"class":303},[283,8934,610],{"class":299},[283,8936,4624],{"class":303},[283,8938,3801],{"class":299},[283,8940,613],{"class":303},[283,8942,8943,8945,8947,8950,8953,8956,8958,8960,8963,8966,8968,8971,8973,8975],{"class":285,"line":654},[283,8944,706],{"class":289},[283,8946,6185],{"class":895},[283,8948,8949],{"class":289},"^",[283,8951,8952],{"class":320},"[A-Z]",[283,8954,8955],{"class":289},"{2}$",[283,8957,25],{"class":895},[283,8959,60],{"class":303},[283,8961,8962],{"class":299},"test",[283,8964,8965],{"class":303},"(code) ",[283,8967,2746],{"class":289},[283,8969,8970],{"class":303}," code ",[283,8972,317],{"class":289},[283,8974,2232],{"class":320},[283,8976,483],{"class":303},[283,8978,8979,8981,8983],{"class":285,"line":675},[283,8980,7601],{"class":303},[283,8982,1925],{"class":289},[283,8984,471],{"class":303},[283,8986,8987,8989,8991],{"class":285,"line":703},[283,8988,706],{"class":289},[283,8990,2232],{"class":320},[283,8992,483],{"class":303},[283,8994,8995,8997,9000],{"class":285,"line":725},[283,8996,7601],{"class":303},[283,8998,8999],{"class":289},"finally",[283,9001,471],{"class":303},[283,9003,9004,9007],{"class":285,"line":731},[283,9005,9006],{"class":299},"    clearTimeout",[283,9008,9009],{"class":303},"(timer);\n",[283,9011,9012],{"class":285,"line":1377},[283,9013,1453],{"class":303},[283,9015,9016],{"class":285,"line":1402},[283,9017,440],{"class":303},[10,9019,9020,9022,9023,9025,9026,9029,9030,9033,9034,9037,9038,9037,9041,9043],{},[14,9021,20],{}," plus a 2-second timer means a slow IP service can't block the detection chain for longer than ",[14,9024,8736],{},". ",[14,9027,9028],{},"credentials: 'omit'"," keeps cookies out of the request to whatever third party you've configured. The shape of the response is checked with ",[14,9031,9032],{},"\u002F^[A-Z]{2}$\u002F"," because some IP APIs return ",[14,9035,9036],{},"\"GB\"",", some return ",[14,9039,9040],{},"\"United Kingdom\"",[14,9042,2809],{}," when they don't know. The regex is the gate.",[10,9045,9046,9049],{},[14,9047,9048],{},"detectCountry"," lives outside the component on purpose:",[274,9051,9053],{"className":276,"code":9052,"language":278,"meta":279,"style":279},"import { detectCountry } from '@alikhalilll\u002Fui\u002Ftell-input';\n\nconst country = await detectCountry({ strategy: 'locale' });\n",[14,9054,9055,9069,9073],{"__ignoreMap":279},[283,9056,9057,9059,9062,9064,9067],{"class":285,"line":286},[283,9058,5420],{"class":289},[283,9060,9061],{"class":303}," { detectCountry } ",[283,9063,5431],{"class":289},[283,9065,9066],{"class":895}," '@alikhalilll\u002Fui\u002Ftell-input'",[283,9068,483],{"class":303},[283,9070,9071],{"class":285,"line":352},[283,9072,543],{"emptyLinePlaceholder":542},[283,9074,9075,9077,9079,9081,9083,9085,9088,9091],{"class":285,"line":372},[283,9076,769],{"class":289},[283,9078,7073],{"class":320},[283,9080,334],{"class":289},[283,9082,363],{"class":289},[283,9084,8329],{"class":299},[283,9086,9087],{"class":303},"({ strategy: ",[283,9089,9090],{"class":895},"'locale'",[283,9092,979],{"class":303},[10,9094,9095,9096,9099,9100,9103],{},"No Vue dependency, no reactive refs, no ",[14,9097,9098],{},"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,9101,9102],{},"useCountryDetection",") is a thin shell around it for the in-component case. Two surfaces, one implementation.",[10,9105,9106],{},"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:",[274,9108,9110],{"className":5177,"code":9109,"language":5179,"meta":279,"style":279},"\u003CATellInput\n  v-model:phone=\"phone\"\n  v-model:country=\"country\"\n  :detector=\"(opts) => Promise.resolve(serverDetectedCountry.value)\"\n\u002F>\n",[14,9111,9112,9119,9138,9154,9185],{"__ignoreMap":279},[283,9113,9114,9116],{"class":285,"line":286},[283,9115,304],{"class":303},[283,9117,9118],{"class":5188},"ATellInput\n",[283,9120,9121,9124,9126,9129,9131,9133,9135],{"class":285,"line":352},[283,9122,9123],{"class":299},"  v-model",[283,9125,317],{"class":303},[283,9127,9128],{"class":299},"phone",[283,9130,533],{"class":303},[283,9132,5197],{"class":895},[283,9134,9128],{"class":303},[283,9136,9137],{"class":895},"\"\n",[283,9139,9140,9142,9144,9146,9148,9150,9152],{"class":285,"line":372},[283,9141,9123],{"class":299},[283,9143,317],{"class":303},[283,9145,7003],{"class":299},[283,9147,533],{"class":303},[283,9149,5197],{"class":895},[283,9151,7003],{"class":303},[283,9153,9137],{"class":895},[283,9155,9156,9159,9162,9164,9166,9168,9170,9172,9174,9176,9178,9180,9183],{"class":285,"line":411},[283,9157,9158],{"class":303},"  :",[283,9160,9161],{"class":299},"detector",[283,9163,533],{"class":303},[283,9165,5197],{"class":895},[283,9167,463],{"class":303},[283,9169,8334],{"class":313},[283,9171,1116],{"class":303},[283,9173,949],{"class":289},[283,9175,342],{"class":320},[283,9177,60],{"class":303},[283,9179,2915],{"class":299},[283,9181,9182],{"class":303},"(serverDetectedCountry.value)",[283,9184,9137],{"class":895},[283,9186,9187],{"class":285,"line":437},[283,9188,9189],{"class":303},"\u002F>\n",[10,9191,2580,9192,9194,9195,9198],{},[14,9193,9161],{}," 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 ",[1017,9196,9197],{},"source"," of the answer becomes whatever you said it was.",[94,9200,9202],{"id":9201},"responsive-picker-one-component-two-presentations","Responsive picker: one component, two presentations",[10,9204,9205],{},"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,9207,9208,9209,9212],{},"The new picker uses the ",[14,9210,9211],{},"AResponsivePopover"," primitive from the same library, which is a popover on desktop and a vaul-vue drawer on mobile:",[274,9214,9216],{"className":5177,"code":9215,"language":5179,"meta":279,"style":279},"\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,9217,9218,9241,9246,9251,9256,9260,9265,9270,9275,9280,9285,9290,9295,9300,9305,9310,9315],{"__ignoreMap":279},[283,9219,9220,9222,9224,9227,9229,9231,9233,9235,9237,9239],{"class":285,"line":286},[283,9221,304],{"class":303},[283,9223,9211],{"class":5188},[283,9225,9226],{"class":299}," v-model",[283,9228,317],{"class":303},[283,9230,2950],{"class":299},[283,9232,533],{"class":303},[283,9234,5197],{"class":895},[283,9236,2950],{"class":303},[283,9238,5197],{"class":895},[283,9240,5205],{"class":303},[283,9242,9243],{"class":285,"line":352},[283,9244,9245],{"class":303},"  \u003CAResponsivePopoverTrigger as-child>\n",[283,9247,9248],{"class":285,"line":372},[283,9249,9250],{"class":303},"    \u003C!-- trigger button -->\n",[283,9252,9253],{"class":285,"line":411},[283,9254,9255],{"class":303},"  \u003C\u002FAResponsivePopoverTrigger>\n",[283,9257,9258],{"class":285,"line":437},[283,9259,543],{"emptyLinePlaceholder":542},[283,9261,9262],{"class":285,"line":539},[283,9263,9264],{"class":303},"  \u003CAResponsivePopoverContent\n",[283,9266,9267],{"class":285,"line":546},[283,9268,9269],{"class":303},"    :popover-class=\"\n",[283,9271,9272],{"class":285,"line":591},[283,9273,9274],{"class":303},"      cn(\n",[283,9276,9277],{"class":285,"line":616},[283,9278,9279],{"class":303},"        'w-[min(20rem,calc(100vw-2rem))] max-h-[min(22rem,var(--reka-popover-content-available-height))]',\n",[283,9281,9282],{"class":285,"line":654},[283,9283,9284],{"class":303},"        props.popoverClass\n",[283,9286,9287],{"class":285,"line":675},[283,9288,9289],{"class":303},"      )\n",[283,9291,9292],{"class":285,"line":703},[283,9293,9294],{"class":303},"    \"\n",[283,9296,9297],{"class":285,"line":725},[283,9298,9299],{"class":303},"    :drawer-class=\"cn('max-h-[80vh] pb-4', props.drawerClass)\"\n",[283,9301,9302],{"class":285,"line":731},[283,9303,9304],{"class":303},"  >\n",[283,9306,9307],{"class":285,"line":1377},[283,9308,9309],{"class":303},"    \u003C!-- search + list -->\n",[283,9311,9312],{"class":285,"line":1402},[283,9313,9314],{"class":303},"  \u003C\u002FAResponsivePopoverContent>\n",[283,9316,9317,9319,9321],{"class":285,"line":1429},[283,9318,5220],{"class":303},[283,9320,9211],{"class":5188},[283,9322,5205],{"class":303},[10,9324,9325,9326,4804,9329,9332,9333,9336,9337,9340],{},"Two class props (",[14,9327,9328],{},"popoverClass",[14,9330,9331],{},"drawerClass",") get forwarded separately, so consumers can theme each presentation without the other inheriting it. The popover wants ",[14,9334,9335],{},"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,9338,9339],{},"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,9342,9343],{},"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.",[94,9345,9347],{"id":9346},"slots-for-everything-fourteen-of-them","Slots for everything (fourteen of them)",[10,9349,9350],{},"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,9352,9353],{},"The new version owns the behaviour and lets the consumer own the look:",[274,9355,9357],{"className":276,"code":9356,"language":278,"meta":279,"style":279},"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,9358,9359,9367,9382,9433,9448,9478,9529,9575,9627,9656,9700,9760,9805,9865,9880,9910],{"__ignoreMap":279},[283,9360,9361,9364],{"class":285,"line":286},[283,9362,9363],{"class":299},"defineSlots",[283,9365,9366],{"class":303},"\u003C{\n",[283,9368,9369,9372,9374,9376,9378,9380],{"class":285,"line":352},[283,9370,9371],{"class":299},"  prefix",[283,9373,491],{"class":289},[283,9375,946],{"class":303},[283,9377,949],{"class":289},[283,9379,1864],{"class":320},[283,9381,483],{"class":303},[283,9383,9384,9387,9389,9391,9393,9395,9397,9400,9402,9405,9407,9410,9412,9415,9417,9420,9422,9425,9427,9429,9431],{"class":285,"line":372},[283,9385,9386],{"class":299},"  suffix",[283,9388,491],{"class":289},[283,9390,378],{"class":303},[283,9392,5820],{"class":313},[283,9394,317],{"class":289},[283,9396,517],{"class":303},[283,9398,9399],{"class":313},"validationState",[283,9401,317],{"class":289},[283,9403,9404],{"class":895}," 'idle'",[283,9406,848],{"class":289},[283,9408,9409],{"class":895}," 'valid'",[283,9411,848],{"class":289},[283,9413,9414],{"class":895}," 'error'",[283,9416,1069],{"class":303},[283,9418,9419],{"class":313},"validation",[283,9421,317],{"class":289},[283,9423,9424],{"class":299}," PhoneValidationResult",[283,9426,1078],{"class":303},[283,9428,949],{"class":289},[283,9430,1864],{"class":320},[283,9432,483],{"class":303},[283,9434,9435,9438,9440,9442,9444,9446],{"class":285,"line":411},[283,9436,9437],{"class":895},"  'valid-icon'",[283,9439,491],{"class":289},[283,9441,946],{"class":303},[283,9443,949],{"class":289},[283,9445,1864],{"class":320},[283,9447,483],{"class":303},[283,9449,9450,9453,9455,9457,9459,9461,9463,9466,9468,9470,9472,9474,9476],{"class":285,"line":437},[283,9451,9452],{"class":895},"  'error-icon'",[283,9454,491],{"class":289},[283,9456,378],{"class":303},[283,9458,5820],{"class":313},[283,9460,317],{"class":289},[283,9462,517],{"class":303},[283,9464,9465],{"class":313},"reason",[283,9467,317],{"class":289},[283,9469,321],{"class":320},[283,9471,1078],{"class":303},[283,9473,949],{"class":289},[283,9475,1864],{"class":320},[283,9477,483],{"class":303},[283,9479,9480,9483,9485,9487,9489,9491,9493,9495,9497,9499,9501,9504,9506,9508,9510,9513,9515,9517,9519,9521,9523,9525,9527],{"class":285,"line":539},[283,9481,9482],{"class":299},"  hint",[283,9484,491],{"class":289},[283,9486,378],{"class":303},[283,9488,5820],{"class":313},[283,9490,317],{"class":289},[283,9492,517],{"class":303},[283,9494,7003],{"class":313},[283,9496,317],{"class":289},[283,9498,321],{"class":320},[283,9500,1069],{"class":303},[283,9502,9503],{"class":313},"formatHint",[283,9505,317],{"class":289},[283,9507,321],{"class":320},[283,9509,1069],{"class":303},[283,9511,9512],{"class":313},"example",[283,9514,317],{"class":289},[283,9516,321],{"class":320},[283,9518,848],{"class":289},[283,9520,2232],{"class":320},[283,9522,1078],{"class":303},[283,9524,949],{"class":289},[283,9526,1864],{"class":320},[283,9528,483],{"class":303},[283,9530,9531,9534,9536,9538,9540,9542,9544,9547,9549,9551,9553,9555,9557,9559,9561,9563,9565,9567,9569,9571,9573],{"class":285,"line":546},[283,9532,9533],{"class":299},"  error",[283,9535,491],{"class":289},[283,9537,378],{"class":303},[283,9539,5820],{"class":313},[283,9541,317],{"class":289},[283,9543,517],{"class":303},[283,9545,9546],{"class":313},"message",[283,9548,317],{"class":289},[283,9550,321],{"class":320},[283,9552,1069],{"class":303},[283,9554,9465],{"class":313},[283,9556,317],{"class":289},[283,9558,321],{"class":320},[283,9560,1069],{"class":303},[283,9562,9419],{"class":313},[283,9564,317],{"class":289},[283,9566,9424],{"class":299},[283,9568,1078],{"class":303},[283,9570,949],{"class":289},[283,9572,1864],{"class":320},[283,9574,483],{"class":303},[283,9576,9577,9580,9582,9584,9586,9588,9590,9593,9595,9598,9600,9602,9604,9606,9608,9610,9612,9615,9617,9619,9621,9623,9625],{"class":285,"line":591},[283,9578,9579],{"class":299},"  trigger",[283,9581,491],{"class":289},[283,9583,378],{"class":303},[283,9585,5820],{"class":313},[283,9587,317],{"class":289},[283,9589,517],{"class":303},[283,9591,9592],{"class":313},"selectedCountry",[283,9594,317],{"class":289},[283,9596,9597],{"class":299}," CountryOption",[283,9599,848],{"class":289},[283,9601,2232],{"class":320},[283,9603,1069],{"class":303},[283,9605,2950],{"class":313},[283,9607,317],{"class":289},[283,9609,2620],{"class":320},[283,9611,1069],{"class":303},[283,9613,9614],{"class":313},"sizeClasses",[283,9616,317],{"class":289},[283,9618,321],{"class":320},[283,9620,1078],{"class":303},[283,9622,949],{"class":289},[283,9624,1864],{"class":320},[283,9626,483],{"class":303},[283,9628,9629,9632,9634,9636,9638,9640,9642,9644,9646,9648,9650,9652,9654],{"class":285,"line":616},[283,9630,9631],{"class":299},"  chevron",[283,9633,491],{"class":289},[283,9635,378],{"class":303},[283,9637,5820],{"class":313},[283,9639,317],{"class":289},[283,9641,517],{"class":303},[283,9643,2950],{"class":313},[283,9645,317],{"class":289},[283,9647,2620],{"class":320},[283,9649,1078],{"class":303},[283,9651,949],{"class":289},[283,9653,1864],{"class":320},[283,9655,483],{"class":303},[283,9657,9658,9661,9663,9665,9667,9669,9671,9673,9675,9677,9679,9682,9684,9687,9689,9692,9694,9696,9698],{"class":285,"line":654},[283,9659,9660],{"class":299},"  flag",[283,9662,491],{"class":289},[283,9664,378],{"class":303},[283,9666,5820],{"class":313},[283,9668,317],{"class":289},[283,9670,517],{"class":303},[283,9672,7003],{"class":313},[283,9674,317],{"class":289},[283,9676,9597],{"class":299},[283,9678,1069],{"class":303},[283,9680,9681],{"class":313},"context",[283,9683,317],{"class":289},[283,9685,9686],{"class":895}," 'trigger'",[283,9688,848],{"class":289},[283,9690,9691],{"class":895}," 'item'",[283,9693,1078],{"class":303},[283,9695,949],{"class":289},[283,9697,1864],{"class":320},[283,9699,483],{"class":303},[283,9701,9702,9705,9707,9709,9711,9713,9715,9717,9719,9721,9723,9726,9728,9730,9732,9735,9737,9739,9741,9744,9746,9748,9750,9752,9754,9756,9758],{"class":285,"line":675},[283,9703,9704],{"class":299},"  item",[283,9706,491],{"class":289},[283,9708,378],{"class":303},[283,9710,5820],{"class":313},[283,9712,317],{"class":289},[283,9714,517],{"class":303},[283,9716,7003],{"class":313},[283,9718,317],{"class":289},[283,9720,9597],{"class":299},[283,9722,1069],{"class":303},[283,9724,9725],{"class":313},"selected",[283,9727,317],{"class":289},[283,9729,2620],{"class":320},[283,9731,1069],{"class":303},[283,9733,9734],{"class":313},"disabled",[283,9736,317],{"class":289},[283,9738,2620],{"class":320},[283,9740,1069],{"class":303},[283,9742,9743],{"class":299},"select",[283,9745,317],{"class":289},[283,9747,946],{"class":303},[283,9749,949],{"class":289},[283,9751,1083],{"class":320},[283,9753,1078],{"class":303},[283,9755,949],{"class":289},[283,9757,1864],{"class":320},[283,9759,483],{"class":303},[283,9761,9762,9765,9767,9769,9771,9773,9775,9778,9780,9782,9784,9787,9789,9792,9794,9797,9799,9801,9803],{"class":285,"line":703},[283,9763,9764],{"class":895},"  'group-header'",[283,9766,491],{"class":289},[283,9768,378],{"class":303},[283,9770,5820],{"class":313},[283,9772,317],{"class":289},[283,9774,517],{"class":303},[283,9776,9777],{"class":313},"label",[283,9779,317],{"class":289},[283,9781,321],{"class":320},[283,9783,1069],{"class":303},[283,9785,9786],{"class":313},"group",[283,9788,317],{"class":289},[283,9790,9791],{"class":895}," 'suggested'",[283,9793,848],{"class":289},[283,9795,9796],{"class":895}," 'all'",[283,9798,1078],{"class":303},[283,9800,949],{"class":289},[283,9802,1864],{"class":320},[283,9804,483],{"class":303},[283,9806,9807,9810,9812,9814,9816,9818,9820,9822,9824,9826,9828,9831,9833,9835,9838,9840,9842,9844,9846,9848,9850,9853,9855,9857,9859,9861,9863],{"class":285,"line":725},[283,9808,9809],{"class":299},"  search",[283,9811,491],{"class":289},[283,9813,378],{"class":303},[283,9815,5820],{"class":313},[283,9817,317],{"class":289},[283,9819,517],{"class":303},[283,9821,1689],{"class":313},[283,9823,317],{"class":289},[283,9825,321],{"class":320},[283,9827,1069],{"class":303},[283,9829,9830],{"class":299},"setValue",[283,9832,317],{"class":289},[283,9834,378],{"class":303},[283,9836,9837],{"class":313},"v",[283,9839,317],{"class":289},[283,9841,321],{"class":320},[283,9843,1116],{"class":303},[283,9845,949],{"class":289},[283,9847,1083],{"class":320},[283,9849,1069],{"class":303},[283,9851,9852],{"class":313},"isSearching",[283,9854,317],{"class":289},[283,9856,2620],{"class":320},[283,9858,1078],{"class":303},[283,9860,949],{"class":289},[283,9862,1864],{"class":320},[283,9864,483],{"class":303},[283,9866,9867,9870,9872,9874,9876,9878],{"class":285,"line":731},[283,9868,9869],{"class":299},"  loading",[283,9871,491],{"class":289},[283,9873,946],{"class":303},[283,9875,949],{"class":289},[283,9877,1864],{"class":320},[283,9879,483],{"class":303},[283,9881,9882,9885,9887,9889,9891,9893,9895,9898,9900,9902,9904,9906,9908],{"class":285,"line":1377},[283,9883,9884],{"class":299},"  empty",[283,9886,491],{"class":289},[283,9888,378],{"class":303},[283,9890,5820],{"class":313},[283,9892,317],{"class":289},[283,9894,517],{"class":303},[283,9896,9897],{"class":313},"query",[283,9899,317],{"class":289},[283,9901,321],{"class":320},[283,9903,1078],{"class":303},[283,9905,949],{"class":289},[283,9907,1864],{"class":320},[283,9909,483],{"class":303},[283,9911,9912],{"class":285,"line":1402},[283,9913,9914],{"class":303},"}>();\n",[10,9916,9917,9918,180,9921,9924,9925,180,9928,9931,9932,180,9935,180,9938,9941,9942,180,9945,180,9947,180,9950,180,9953,9956],{},"Fourteen slots, three groups. The visual ones (",[14,9919,9920],{},"prefix",[14,9922,9923],{},"suffix",", the two icon slots, ",[14,9926,9927],{},"hint",[14,9929,9930],{},"error",") decorate the field itself. The picker-trigger ones (",[14,9933,9934],{},"trigger",[14,9936,9937],{},"chevron",[14,9939,9940],{},"flag",") replace the button users click. The picker-content ones (",[14,9943,9944],{},"search",[14,9946,5382],{},[14,9948,9949],{},"empty",[14,9951,9952],{},"group-header",[14,9954,9955],{},"item",") replace anything inside the popover or drawer.",[10,9958,9959,9960,9963],{},"Almost all of the picker slots forward straight to ",[14,9961,9962],{},"ACountrySelect",". The component itself does the routing:",[274,9965,9967],{"className":5177,"code":9966,"language":5179,"meta":279,"style":279},"\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,9968,9969,10002,10028,10036,10067,10090,10098],{"__ignoreMap":279},[283,9970,9971,9973,9975,9977,9979,9981,9984,9986,9989,9991,9993,9995,9998,10000],{"class":285,"line":286},[283,9972,304],{"class":303},[283,9974,5914],{"class":5188},[283,9976,5192],{"class":289},[283,9978,533],{"class":303},[283,9980,5197],{"class":895},[283,9982,9983],{"class":303},"$slots.trigger",[283,9985,5197],{"class":895},[283,9987,9988],{"class":303}," #",[283,9990,9934],{"class":299},[283,9992,533],{"class":303},[283,9994,5197],{"class":895},[283,9996,9997],{"class":303},"slotProps",[283,9999,5197],{"class":895},[283,10001,5205],{"class":303},[283,10003,10004,10006,10009,10011,10013,10016,10019,10021,10024,10026],{"class":285,"line":352},[283,10005,5921],{"class":303},[283,10007,10008],{"class":5188},"slot",[283,10010,4688],{"class":299},[283,10012,533],{"class":303},[283,10014,10015],{"class":895},"\"trigger\"",[283,10017,10018],{"class":299}," v-bind",[283,10020,533],{"class":303},[283,10022,10023],{"class":895},"\"slotProps\"",[283,10025,6185],{"class":6184},[283,10027,5205],{"class":303},[283,10029,10030,10032,10034],{"class":285,"line":372},[283,10031,5220],{"class":303},[283,10033,5914],{"class":5188},[283,10035,5205],{"class":303},[283,10037,10038,10040,10042,10044,10046,10048,10051,10053,10055,10057,10059,10061,10063,10065],{"class":285,"line":411},[283,10039,304],{"class":303},[283,10041,5914],{"class":5188},[283,10043,5192],{"class":289},[283,10045,533],{"class":303},[283,10047,5197],{"class":895},[283,10049,10050],{"class":303},"$slots.chevron",[283,10052,5197],{"class":895},[283,10054,9988],{"class":303},[283,10056,9937],{"class":299},[283,10058,533],{"class":303},[283,10060,5197],{"class":895},[283,10062,9997],{"class":303},[283,10064,5197],{"class":895},[283,10066,5205],{"class":303},[283,10068,10069,10071,10073,10075,10077,10080,10082,10084,10086,10088],{"class":285,"line":437},[283,10070,5921],{"class":303},[283,10072,10008],{"class":5188},[283,10074,4688],{"class":299},[283,10076,533],{"class":303},[283,10078,10079],{"class":895},"\"chevron\"",[283,10081,10018],{"class":299},[283,10083,533],{"class":303},[283,10085,10023],{"class":895},[283,10087,6185],{"class":6184},[283,10089,5205],{"class":303},[283,10091,10092,10094,10096],{"class":285,"line":539},[283,10093,5220],{"class":303},[283,10095,5914],{"class":5188},[283,10097,5205],{"class":303},[283,10099,10100],{"class":285,"line":546},[283,10101,10102],{"class":917},"\u003C!-- ... and so on for each forwarded slot -->\n",[10,10104,2580,10105,10108,10109,10112,10113,10115],{},[14,10106,10107],{},"v-if=\"$slots.x\""," matters. Without it, a forwarded ",[14,10110,10111],{},"\u003Ctemplate #x>"," always exists from the parent's perspective, and ",[14,10114,9962],{},"'s own fallback rendering never gets a chance to run. The guard says \"only forward if the consumer actually provided this slot.\"",[10,10117,10118],{},"A realistic customization:",[274,10120,10122],{"className":5177,"code":10121,"language":5179,"meta":279,"style":279},"\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,10123,10124,10163,10168,10173,10178,10183,10188,10193,10198,10202,10207,10212,10217,10222,10227,10232,10237,10241],{"__ignoreMap":279},[283,10125,10126,10128,10130,10132,10134,10136,10138,10140,10142,10144,10146,10148,10150,10152,10154,10156,10158,10161],{"class":285,"line":286},[283,10127,304],{"class":303},[283,10129,6918],{"class":5188},[283,10131,9226],{"class":299},[283,10133,317],{"class":303},[283,10135,9128],{"class":299},[283,10137,533],{"class":303},[283,10139,5197],{"class":895},[283,10141,9128],{"class":303},[283,10143,5197],{"class":895},[283,10145,9226],{"class":299},[283,10147,317],{"class":303},[283,10149,7003],{"class":299},[283,10151,533],{"class":303},[283,10153,5197],{"class":895},[283,10155,7003],{"class":303},[283,10157,5197],{"class":895},[283,10159,10160],{"class":299}," show-validation",[283,10162,5205],{"class":303},[283,10164,10165],{"class":285,"line":352},[283,10166,10167],{"class":303},"  \u003Ctemplate #suffix=\"{ validationState }\">\n",[283,10169,10170],{"class":285,"line":372},[283,10171,10172],{"class":303},"    \u003CSparkle v-if=\"validationState === 'valid'\" class=\"size-4 text-amber-400\" \u002F>\n",[283,10174,10175],{"class":285,"line":411},[283,10176,10177],{"class":303},"  \u003C\u002Ftemplate>\n",[283,10179,10180],{"class":285,"line":437},[283,10181,10182],{"class":303},"  \u003Ctemplate #error=\"{ message, reason }\">\n",[283,10184,10185],{"class":285,"line":539},[283,10186,10187],{"class":303},"    \u003Cp class=\"text-destructive text-xs\">\n",[283,10189,10190],{"class":285,"line":546},[283,10191,10192],{"class":303},"      \u003Ccode class=\"bg-muted rounded px-1 py-0.5\">{{ reason }}\u003C\u002Fcode> {{ message }}\n",[283,10194,10195],{"class":285,"line":591},[283,10196,10197],{"class":303},"    \u003C\u002Fp>\n",[283,10199,10200],{"class":285,"line":616},[283,10201,10177],{"class":303},[283,10203,10204],{"class":285,"line":654},[283,10205,10206],{"class":303},"  \u003Ctemplate #empty=\"{ query }\">\n",[283,10208,10209],{"class":285,"line":675},[283,10210,10211],{"class":303},"    \u003Cdiv class=\"px-3 py-6 text-center text-sm\">\n",[283,10213,10214],{"class":285,"line":703},[283,10215,10216],{"class":303},"      Nothing matched \"\u003Cstrong>{{ query }}\u003C\u002Fstrong>\".\n",[283,10218,10219],{"class":285,"line":725},[283,10220,10221],{"class":303},"      \u003Cbutton type=\"button\" class=\"underline\" @click=\"suggestCountry(query)\">\n",[283,10223,10224],{"class":285,"line":731},[283,10225,10226],{"class":303},"        Suggest a country\n",[283,10228,10229],{"class":285,"line":1377},[283,10230,10231],{"class":303},"      \u003C\u002Fbutton>\n",[283,10233,10234],{"class":285,"line":1402},[283,10235,10236],{"class":303},"    \u003C\u002Fdiv>\n",[283,10238,10239],{"class":285,"line":1429},[283,10240,10177],{"class":303},[283,10242,10243,10245,10247],{"class":285,"line":1450},[283,10244,5220],{"class":303},[283,10246,6918],{"class":5188},[283,10248,5205],{"class":303},[10,10250,10251,10252,10255],{},"A library component's job is to own ",[1017,10253,10254],{},"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,10257,10258],{},"The same principle applies to the props that aren't visual:",[274,10260,10262],{"className":276,"code":10261,"language":278,"meta":279,"style":279},"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,10263,10264,10296,10326,10336,10373],{"__ignoreMap":279},[283,10265,10266,10269,10271,10273,10276,10278,10280,10282,10285,10287,10289,10291,10293],{"class":285,"line":286},[283,10267,10268],{"class":303},"flagUrl",[283,10270,491],{"class":289},[283,10272,378],{"class":303},[283,10274,10275],{"class":313},"iso2",[283,10277,317],{"class":289},[283,10279,321],{"class":320},[283,10281,180],{"class":303},[283,10283,10284],{"class":313},"width",[283,10286,317],{"class":289},[283,10288,2632],{"class":320},[283,10290,1116],{"class":303},[283,10292,949],{"class":289},[283,10294,10295],{"class":303}," string;\n",[283,10297,10298,10301,10303,10305,10307,10309,10311,10313,10315,10317,10319,10321,10323],{"class":285,"line":352},[283,10299,10300],{"class":303},"searcher",[283,10302,491],{"class":289},[283,10304,378],{"class":303},[283,10306,9897],{"class":313},[283,10308,317],{"class":289},[283,10310,321],{"class":320},[283,10312,180],{"class":303},[283,10314,7003],{"class":313},[283,10316,317],{"class":289},[283,10318,9597],{"class":299},[283,10320,1116],{"class":303},[283,10322,949],{"class":289},[283,10324,10325],{"class":303}," boolean;\n",[283,10327,10328,10331,10333],{"class":285,"line":372},[283,10329,10330],{"class":303},"countries",[283,10332,491],{"class":289},[283,10334,10335],{"class":303}," CountryOption[];\n",[283,10337,10338,10340,10342,10344,10346,10348,10350,10352,10354,10356,10358,10361,10363,10365,10367,10369,10371],{"class":285,"line":411},[283,10339,9161],{"class":303},[283,10341,491],{"class":289},[283,10343,378],{"class":303},[283,10345,466],{"class":313},[283,10347,317],{"class":289},[283,10349,8339],{"class":299},[283,10351,1116],{"class":303},[283,10353,949],{"class":289},[283,10355,342],{"class":320},[283,10357,304],{"class":289},[283,10359,10360],{"class":303},"string ",[283,10362,3861],{"class":289},[283,10364,2232],{"class":320},[283,10366,848],{"class":289},[283,10368,851],{"class":320},[283,10370,2775],{"class":289},[283,10372,483],{"class":303},[283,10374,10375,10378,10380,10382,10384,10387,10389],{"class":285,"line":437},[283,10376,10377],{"class":303},"errorMessages",[283,10379,491],{"class":289},[283,10381,494],{"class":303},[283,10383,304],{"class":289},[283,10385,10386],{"class":303},"PhoneValidationReason, string",[283,10388,2775],{"class":289},[283,10390,483],{"class":303},[10,10392,10393,10395,10396,10398,10399,10401,10402,10404],{},[14,10394,10268],{}," lets you swap flagcdn.com for your own CDN; useful if you have a CSP that won't allow third-party images. ",[14,10397,10300],{}," lets you replace substring-matching with prefix-matching, fuzzy-matching, Arabic-name matching, whatever your users actually want. ",[14,10400,10330],{}," 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,10403,10377],{}," is i18n: pass an Arabic table and the validation reasons render in Arabic.",[10,10406,10407],{},"Every prop has a sensible default. The zero-config call still works.",[94,10409,10411],{"id":10410},"the-synchronous-dial-table-and-why-its-not-optional","The synchronous dial table, and why it's not optional",[10,10413,10414,10415,10418,10419,180,10422,10425,10426,10429,10430,10433,10434,8670,10436,10438],{},"The first time the component mounts in a fresh tab, the REST Countries fetch hasn't completed. Whatever indices ",[14,10416,10417],{},"usePhoneValidation"," builds (",[14,10420,10421],{},"byValue",[14,10423,10424],{},"byDialDigits",") are empty maps. If the consumer wrote ",[14,10427,10428],{},"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,10431,10432],{},"[]",", the watcher would set ",[14,10435,7145],{},[14,10437,7064],{},", and Egypt wouldn't appear until the network call returned.",[10,10440,10441],{},"So the component bakes a synchronous fallback table:",[274,10443,10445],{"className":276,"code":10444,"language":278,"meta":279,"style":279},"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,10446,10447,10472,10484,10496,10508,10520,10532,10544,10556,10568,10580,10592,10604,10616,10628,10640,10652,10664,10669],{"__ignoreMap":279},[283,10448,10449,10451,10454,10456,10458,10460,10462,10464,10466,10468,10470],{"class":285,"line":286},[283,10450,769],{"class":289},[283,10452,10453],{"class":320}," DIAL_TO_ISO2_FALLBACK",[283,10455,317],{"class":289},[283,10457,494],{"class":299},[283,10459,304],{"class":303},[283,10461,499],{"class":320},[283,10463,180],{"class":303},[283,10465,499],{"class":320},[283,10467,3858],{"class":303},[283,10469,533],{"class":289},[283,10471,471],{"class":303},[283,10473,10474,10477,10479,10482],{"class":285,"line":352},[283,10475,10476],{"class":895},"  '1'",[283,10478,6691],{"class":303},[283,10480,10481],{"class":895},"'US'",[283,10483,2048],{"class":303},[283,10485,10486,10489,10491,10494],{"class":285,"line":372},[283,10487,10488],{"class":895},"  '7'",[283,10490,6691],{"class":303},[283,10492,10493],{"class":895},"'RU'",[283,10495,2048],{"class":303},[283,10497,10498,10501,10503,10506],{"class":285,"line":411},[283,10499,10500],{"class":895},"  '20'",[283,10502,6691],{"class":303},[283,10504,10505],{"class":895},"'EG'",[283,10507,2048],{"class":303},[283,10509,10510,10513,10515,10518],{"class":285,"line":437},[283,10511,10512],{"class":895},"  '27'",[283,10514,6691],{"class":303},[283,10516,10517],{"class":895},"'ZA'",[283,10519,2048],{"class":303},[283,10521,10522,10525,10527,10530],{"class":285,"line":539},[283,10523,10524],{"class":895},"  '33'",[283,10526,6691],{"class":303},[283,10528,10529],{"class":895},"'FR'",[283,10531,2048],{"class":303},[283,10533,10534,10537,10539,10542],{"class":285,"line":546},[283,10535,10536],{"class":895},"  '44'",[283,10538,6691],{"class":303},[283,10540,10541],{"class":895},"'GB'",[283,10543,2048],{"class":303},[283,10545,10546,10549,10551,10554],{"class":285,"line":591},[283,10547,10548],{"class":895},"  '49'",[283,10550,6691],{"class":303},[283,10552,10553],{"class":895},"'DE'",[283,10555,2048],{"class":303},[283,10557,10558,10561,10563,10566],{"class":285,"line":616},[283,10559,10560],{"class":895},"  '52'",[283,10562,6691],{"class":303},[283,10564,10565],{"class":895},"'MX'",[283,10567,2048],{"class":303},[283,10569,10570,10573,10575,10578],{"class":285,"line":654},[283,10571,10572],{"class":895},"  '55'",[283,10574,6691],{"class":303},[283,10576,10577],{"class":895},"'BR'",[283,10579,2048],{"class":303},[283,10581,10582,10585,10587,10590],{"class":285,"line":675},[283,10583,10584],{"class":895},"  '61'",[283,10586,6691],{"class":303},[283,10588,10589],{"class":895},"'AU'",[283,10591,2048],{"class":303},[283,10593,10594,10597,10599,10602],{"class":285,"line":703},[283,10595,10596],{"class":895},"  '81'",[283,10598,6691],{"class":303},[283,10600,10601],{"class":895},"'JP'",[283,10603,2048],{"class":303},[283,10605,10606,10609,10611,10614],{"class":285,"line":725},[283,10607,10608],{"class":895},"  '86'",[283,10610,6691],{"class":303},[283,10612,10613],{"class":895},"'CN'",[283,10615,2048],{"class":303},[283,10617,10618,10621,10623,10626],{"class":285,"line":731},[283,10619,10620],{"class":895},"  '91'",[283,10622,6691],{"class":303},[283,10624,10625],{"class":895},"'IN'",[283,10627,2048],{"class":303},[283,10629,10630,10633,10635,10638],{"class":285,"line":1377},[283,10631,10632],{"class":895},"  '212'",[283,10634,6691],{"class":303},[283,10636,10637],{"class":895},"'MA'",[283,10639,2048],{"class":303},[283,10641,10642,10645,10647,10650],{"class":285,"line":1402},[283,10643,10644],{"class":895},"  '966'",[283,10646,6691],{"class":303},[283,10648,10649],{"class":895},"'SA'",[283,10651,2048],{"class":303},[283,10653,10654,10657,10659,10662],{"class":285,"line":1429},[283,10655,10656],{"class":895},"  '971'",[283,10658,6691],{"class":303},[283,10660,10661],{"class":895},"'AE'",[283,10663,2048],{"class":303},[283,10665,10666],{"class":285,"line":1450},[283,10667,10668],{"class":917},"  \u002F\u002F ... ~50 entries\n",[283,10670,10671],{"class":285,"line":1456},[283,10672,10673],{"class":303},"};\n",[10,10675,10676,10677,10680],{},"And ",[14,10678,10679],{},"resolveCountryIdentifier"," checks the loaded index first, then this table:",[274,10682,10684],{"className":276,"code":10683,"language":278,"meta":279,"style":279},"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,10685,10686,10716,10741,10758,10791,10823,10856,10874,10886,10901],{"__ignoreMap":279},[283,10687,10688,10690,10692,10694,10696,10698,10700,10702,10704,10706,10708,10710,10712,10714],{"class":285,"line":286},[283,10689,816],{"class":289},[283,10691,7248],{"class":299},[283,10693,463],{"class":303},[283,10695,4558],{"class":313},[283,10697,317],{"class":289},[283,10699,321],{"class":320},[283,10701,848],{"class":289},[283,10703,851],{"class":320},[283,10705,848],{"class":289},[283,10707,2232],{"class":320},[283,10709,841],{"class":303},[283,10711,317],{"class":289},[283,10713,321],{"class":320},[283,10715,471],{"class":303},[283,10717,10718,10720,10723,10725,10728,10731,10733,10735,10737,10739],{"class":285,"line":352},[283,10719,355],{"class":289},[283,10721,10722],{"class":320}," v",[283,10724,334],{"class":289},[283,10726,10727],{"class":299}," String",[283,10729,10730],{"class":303},"(raw ",[283,10732,1305],{"class":289},[283,10734,7203],{"class":895},[283,10736,3798],{"class":303},[283,10738,4621],{"class":299},[283,10740,613],{"class":303},[283,10742,10743,10745,10747,10749,10752,10754,10756],{"class":285,"line":372},[283,10744,375],{"class":289},[283,10746,378],{"class":303},[283,10748,381],{"class":289},[283,10750,10751],{"class":303},"v) ",[283,10753,867],{"class":289},[283,10755,7203],{"class":895},[283,10757,483],{"class":303},[283,10759,10760,10762,10764,10766,10768,10771,10773,10775,10777,10779,10782,10784,10787,10789],{"class":285,"line":411},[283,10761,375],{"class":289},[283,10763,378],{"class":303},[283,10765,25],{"class":895},[283,10767,8949],{"class":289},[283,10769,10770],{"class":320},"[A-Za-z]",[283,10772,8955],{"class":289},[283,10774,25],{"class":895},[283,10776,60],{"class":303},[283,10778,8962],{"class":299},[283,10780,10781],{"class":303},"(v)) ",[283,10783,867],{"class":289},[283,10785,10786],{"class":303}," v.",[283,10788,3801],{"class":299},[283,10790,613],{"class":303},[283,10792,10793,10795,10798,10800,10802,10805,10807,10809,10811,10815,10817,10819,10821],{"class":285,"line":437},[283,10794,355],{"class":289},[283,10796,10797],{"class":320}," dial",[283,10799,334],{"class":289},[283,10801,10786],{"class":303},[283,10803,10804],{"class":299},"replace",[283,10806,463],{"class":303},[283,10808,25],{"class":895},[283,10810,8949],{"class":289},[283,10812,10814],{"class":10813},"snhLl","\\+",[283,10816,25],{"class":895},[283,10818,180],{"class":303},[283,10820,7064],{"class":895},[283,10822,788],{"class":303},[283,10824,10825,10827,10829,10831,10833,10835,10838,10841,10843,10845,10847,10850,10852,10854],{"class":285,"line":539},[283,10826,375],{"class":289},[283,10828,378],{"class":303},[283,10830,381],{"class":289},[283,10832,25],{"class":895},[283,10834,8949],{"class":289},[283,10836,10837],{"class":320},"\\d",[283,10839,10840],{"class":289},"+$",[283,10842,25],{"class":895},[283,10844,60],{"class":303},[283,10846,8962],{"class":299},[283,10848,10849],{"class":303},"(dial)) ",[283,10851,867],{"class":289},[283,10853,7203],{"class":895},[283,10855,483],{"class":303},[283,10857,10858,10860,10862,10864,10866,10869,10871],{"class":285,"line":546},[283,10859,355],{"class":289},[283,10861,8122],{"class":320},[283,10863,334],{"class":289},[283,10865,7855],{"class":299},[283,10867,10868],{"class":303},"(dial)[",[283,10870,2680],{"class":320},[283,10872,10873],{"class":303},"];\n",[283,10875,10876,10878,10881,10883],{"class":285,"line":591},[283,10877,375],{"class":289},[283,10879,10880],{"class":303}," (match) ",[283,10882,867],{"class":289},[283,10884,10885],{"class":303}," match.value;\n",[283,10887,10888,10890,10892,10895,10897,10899],{"class":285,"line":616},[283,10889,414],{"class":289},[283,10891,10453],{"class":320},[283,10893,10894],{"class":303},"[dial] ",[283,10896,1305],{"class":289},[283,10898,7203],{"class":895},[283,10900,483],{"class":303},[283,10902,10903],{"class":285,"line":654},[283,10904,440],{"class":303},[10,10906,10907],{},"The reverse, going from an ISO2 back to a dial number for the v-model, does the same check:",[274,10909,10911],{"className":276,"code":10910,"language":278,"meta":279,"style":279},"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,10912,10913,10940,10957,10971,11021,11037,11051,11075],{"__ignoreMap":279},[283,10914,10915,10917,10920,10922,10924,10926,10928,10930,10932,10934,10936,10938],{"class":285,"line":286},[283,10916,816],{"class":289},[283,10918,10919],{"class":299}," dialNumberFor",[283,10921,463],{"class":303},[283,10923,10275],{"class":313},[283,10925,317],{"class":289},[283,10927,321],{"class":320},[283,10929,841],{"class":303},[283,10931,317],{"class":289},[283,10933,2632],{"class":320},[283,10935,848],{"class":289},[283,10937,2232],{"class":320},[283,10939,471],{"class":303},[283,10941,10942,10944,10946,10948,10951,10953,10955],{"class":285,"line":352},[283,10943,375],{"class":289},[283,10945,378],{"class":303},[283,10947,381],{"class":289},[283,10949,10950],{"class":303},"iso2) ",[283,10952,867],{"class":289},[283,10954,2232],{"class":320},[283,10956,483],{"class":303},[283,10958,10959,10961,10964,10966,10968],{"class":285,"line":372},[283,10960,355],{"class":289},[283,10962,10963],{"class":320}," fromIndex",[283,10965,334],{"class":289},[283,10967,7559],{"class":299},[283,10969,10970],{"class":303},"(iso2)?.raw_data?.dial_digits;\n",[283,10972,10973,10975,10978,10980,10983,10985,10987,10989,10991,10994,10996,10999,11002,11004,11007,11009,11012,11014,11017,11019],{"class":285,"line":411},[283,10974,355],{"class":289},[283,10976,10977],{"class":320}," digits",[283,10979,334],{"class":289},[283,10981,10982],{"class":303}," fromIndex ",[283,10984,1305],{"class":289},[283,10986,4072],{"class":303},[283,10988,4075],{"class":299},[283,10990,463],{"class":303},[283,10992,10993],{"class":320},"DIAL_TO_ISO2_FALLBACK",[283,10995,3798],{"class":303},[283,10997,10998],{"class":299},"find",[283,11000,11001],{"class":303},"(([, ",[283,11003,9837],{"class":313},[283,11005,11006],{"class":303},"]) ",[283,11008,949],{"class":289},[283,11010,11011],{"class":303}," v ",[283,11013,3935],{"class":289},[283,11015,11016],{"class":303}," iso2)?.[",[283,11018,2680],{"class":320},[283,11020,10873],{"class":303},[283,11022,11023,11025,11027,11029,11031,11033,11035],{"class":285,"line":437},[283,11024,375],{"class":289},[283,11026,378],{"class":303},[283,11028,381],{"class":289},[283,11030,7490],{"class":303},[283,11032,867],{"class":289},[283,11034,2232],{"class":320},[283,11036,483],{"class":303},[283,11038,11039,11041,11044,11046,11048],{"class":285,"line":539},[283,11040,355],{"class":289},[283,11042,11043],{"class":320}," n",[283,11045,334],{"class":289},[283,11047,2212],{"class":299},[283,11049,11050],{"class":303},"(digits);\n",[283,11052,11053,11055,11058,11061,11064,11066,11069,11071,11073],{"class":285,"line":546},[283,11054,414],{"class":289},[283,11056,11057],{"class":303}," Number.",[283,11059,11060],{"class":299},"isFinite",[283,11062,11063],{"class":303},"(n) ",[283,11065,2746],{"class":289},[283,11067,11068],{"class":303}," n ",[283,11070,317],{"class":289},[283,11072,2232],{"class":320},[283,11074,483],{"class":303},[283,11076,11077],{"class":285,"line":591},[283,11078,440],{"class":303},[10,11080,11081,11082,11085],{},"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,11083,11084],{},"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,11087,11088],{},"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.",[94,11090,11092],{"id":11091},"recents-as-the-tiebreaker","Recents as the tiebreaker",[10,11094,11095,11096,11098],{},"The longest-prefix Tier 3 match isn't always unambiguous. ",[14,11097,1525],{}," 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,11100,11101,11102,317],{},"So Tier 3 reads from ",[14,11103,6254],{},[274,11105,11107],{"className":276,"code":11106,"language":278,"meta":279,"style":279},"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,11108,11109,11123,11135,11173,11204,11216],{"__ignoreMap":279},[283,11110,11111,11113,11116,11118,11121],{"class":285,"line":286},[283,11112,769],{"class":289},[283,11114,11115],{"class":320}," recents",[283,11117,334],{"class":289},[283,11119,11120],{"class":299}," readRecents",[283,11122,613],{"class":303},[283,11124,11125,11127,11130,11132],{"class":285,"line":352},[283,11126,769],{"class":289},[283,11128,11129],{"class":320}," recentHit",[283,11131,334],{"class":289},[283,11133,11134],{"class":303}," recents\n",[283,11136,11137,11140,11143,11145,11147,11149,11151,11154,11156,11158,11161,11163,11165,11168,11170],{"class":285,"line":372},[283,11138,11139],{"class":303},"  .",[283,11141,11142],{"class":299},"map",[283,11144,1641],{"class":303},[283,11146,10275],{"class":313},[283,11148,1116],{"class":303},[283,11150,949],{"class":289},[283,11152,11153],{"class":303}," group.",[283,11155,10998],{"class":299},[283,11157,1641],{"class":303},[283,11159,11160],{"class":313},"c",[283,11162,1116],{"class":303},[283,11164,949],{"class":289},[283,11166,11167],{"class":303}," c.value ",[283,11169,3935],{"class":289},[283,11171,11172],{"class":303}," iso2))\n",[283,11174,11175,11177,11179,11181,11183,11185,11187,11190,11193,11195,11198,11201],{"class":285,"line":411},[283,11176,11139],{"class":303},[283,11178,10998],{"class":299},[283,11180,1641],{"class":303},[283,11182,11160],{"class":313},[283,11184,841],{"class":303},[283,11186,317],{"class":289},[283,11188,11189],{"class":313}," c",[283,11191,11192],{"class":289}," is",[283,11194,9597],{"class":299},[283,11196,11197],{"class":289}," =>",[283,11199,11200],{"class":299}," Boolean",[283,11202,11203],{"class":303},"(c));\n",[283,11205,11206,11208,11211,11213],{"class":285,"line":437},[283,11207,8231],{"class":289},[283,11209,11210],{"class":303}," (recentHit) ",[283,11212,867],{"class":289},[283,11214,11215],{"class":303}," { country: recentHit, nationalNumber };\n",[283,11217,11218,11220,11222,11224],{"class":285,"line":539},[283,11219,867],{"class":289},[283,11221,7918],{"class":303},[283,11223,2680],{"class":320},[283,11225,7923],{"class":303},[10,11227,11228,11229,11232,11233,11235],{},"The picker writes to ",[14,11230,11231],{},"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,11234,7020],{}," you type will resolve to Canada. If you've never used it, you get the alphabetical fallback: still wrong, but only once.",[10,11237,11238],{},"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.\"",[94,11240,11242],{"id":11241},"what-the-library-rewrite-was-really-about","What the library rewrite was really about",[10,11244,11245,11246,11249,11250,11253],{},"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,11247,11248],{},"dropLeadingZeros"," setter, the seven ",[14,11251,11252],{},"PhoneValidationReason"," enum values, the LTR-wrapper-over-RTL-input bidi pattern. Those things weren't wrong; they were the foundation.",[10,11255,11256,11257,11260],{},"What the library rewrite added was everything that's specific to ",[1017,11258,11259],{},"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,11262,11263],{},"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:",[274,11265,11267],{"className":5177,"code":11266,"language":5179,"meta":279,"style":279},"\u003CATellInput v-model:phone=\"phone\" v-model:country=\"country\" show-validation \u002F>\n",[14,11268,11269],{"__ignoreMap":279},[283,11270,11271,11273,11275,11277,11279,11281,11283,11285,11287,11289,11291,11293,11295,11297,11299,11301,11303,11305],{"class":285,"line":286},[283,11272,304],{"class":303},[283,11274,6918],{"class":5188},[283,11276,9226],{"class":299},[283,11278,317],{"class":303},[283,11280,9128],{"class":299},[283,11282,533],{"class":303},[283,11284,5197],{"class":895},[283,11286,9128],{"class":303},[283,11288,5197],{"class":895},[283,11290,9226],{"class":299},[283,11292,317],{"class":303},[283,11294,7003],{"class":299},[283,11296,533],{"class":303},[283,11298,5197],{"class":895},[283,11300,7003],{"class":303},[283,11302,5197],{"class":895},[283,11304,10160],{"class":299},[283,11306,5958],{"class":303},[10,11308,11309],{},"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,11311,11312,11313,11316,11317,11320],{},"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,11314,11315],{},"restcountries.com"," hardcoded, every Saudi-and-Egypt fallback, every always-visible dropdown: those weren't wrong ",[1017,11318,11319],{},"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.",[5095,11322,11323],{},"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":279,"searchDepth":352,"depth":352,"links":11325},[11326,11327,11328,11329,11330,11331,11332,11333],{"id":6935,"depth":352,"text":6936},{"id":7319,"depth":352,"text":7320},{"id":8299,"depth":352,"text":8300},{"id":9201,"depth":352,"text":9202},{"id":9346,"depth":352,"text":9347},{"id":10410,"depth":352,"text":10411},{"id":11091,"depth":352,"text":11092},{"id":11241,"depth":352,"text":11242},"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 لكل شيء.",[6895,6901,11338,11339,11340,11341,11342,11343,11344,5137],"phone input","libphonenumber-js","country detection","responsive popover","vaul-vue","defineModel","slots",{},"\u002Fwrites\u002Fatell-input-the-library-rewrite",{"title":6911,"description":11335},"writes\u002Fatell-input-the-library-rewrite","ملاحظات من إعادة كتابة ATellInput كمكتبة مُنشَرة","U2nw1NwLneEoQyPNZeM61sua6if230l-WTmnZI7oQVY",1793901269155]