[{"data":1,"prerenderedAt":8157},["ShallowReactive",2],{"writes-\u002Fwrites\u002Fbuilding-ali-nuxt-toolkit":3,"related-writes-\u002Fwrites\u002Fbuilding-ali-nuxt-toolkit":1864},{"id":4,"title":5,"body":6,"date":1838,"description":1839,"description_ar":1840,"draft":1841,"extension":1842,"image":1843,"keywords":1844,"lang":1857,"meta":1858,"navigation":294,"path":1859,"seo":1860,"stem":1861,"title_ar":1862,"updatedAt":1843,"__hash__":1863},"writes\u002Fwrites\u002Fbuilding-ali-nuxt-toolkit.md","Building ali-nuxt-toolkit, a tour of the internals",{"type":7,"value":8,"toc":1815},"minimark",[9,18,21,26,32,68,71,97,100,104,107,439,455,464,473,484,590,597,604,607,647,650,755,762,766,788,824,827,831,834,899,906,924,931,936,1102,1120,1127,1130,1243,1250,1316,1323,1330,1333,1340,1507,1525,1532,1637,1644,1648,1651,1659,1662,1666,1676,1679,1715,1719,1730,1738,1753,1757,1760,1800,1811],[10,11,12,13,17],"p",{},"A while back I decided to pull the patterns I kept rewriting at work into a small set of Nuxt modules. Nothing novel, just the stuff that everyone on every SaaS team eventually writes: a typed HTTP client, layout-scoped middleware, and a crypto service for locally-stored secrets. Packaging them properly turned into its own project: ",[14,15,16],"strong",{},"ali-nuxt-toolkit",".",[10,19,20],{},"This post is a tour of what's inside and why certain pieces are shaped the way they are. I'll skip the obvious parts and spend most of the words on the details I'd want to read if someone else had written it.",[22,23,25],"h2",{"id":24},"the-shape-of-the-repo","The shape of the repo",[10,27,28,31],{},[29,30,16],"code",{}," is a pnpm monorepo. The top level is roughly:",[33,34,35,46,56,62],"ul",{},[36,37,38,41,42,45],"li",{},[29,39,40],{},"packages\u002F",": three independently published modules under the ",[29,43,44],{},"@alikhalilll"," scope.",[36,47,48,51,52,55],{},[29,49,50],{},"apps\u002Fdocs\u002F",": a Nuxt 4 + ",[29,53,54],{},"@nuxt\u002Fcontent"," site, prerendered to static HTML.",[36,57,58,61],{},[29,59,60],{},"playgrounds\u002Fnuxt\u002F",": a minimal app that wires all three modules together; handy for kicking the tires locally.",[36,63,64,67],{},[29,65,66],{},".github\u002Fworkflows\u002F",": CI (lint, typecheck, matrix build on Node 20 + 22) and a Changesets-driven release pipeline.",[10,69,70],{},"The three packages:",[33,72,73,81,89],{},[36,74,75,80],{},[14,76,77],{},[29,78,79],{},"@alikhalilll\u002Fnuxt-api-provider",": strongly-typed fetch client with an interceptor chain, retry\u002Fbackoff, timeouts, and upload\u002Fdownload progress.",[36,82,83,88],{},[14,84,85],{},[29,86,87],{},"@alikhalilll\u002Fnuxt-auto-middleware",": layout-scoped route middleware with glob patterns, named groups, and per-page overrides.",[36,90,91,96],{},[14,92,93],{},[29,94,95],{},"@alikhalilll\u002Fnuxt-crypto",": AES-256-GCM + PBKDF2 built on Web Crypto, with an LRU key cache and pluggable algorithms.",[10,98,99],{},"They're deliberately small and focused. Each one works standalone. Each one also has a framework-agnostic \"core\" that can run in Node, Bun, Deno, or a test. No Nuxt required.",[22,101,103],{"id":102},"the-module-skeleton","The module skeleton",[10,105,106],{},"All three modules follow the same Nuxt 4 shape:",[108,109,114],"pre",{"className":110,"code":111,"language":112,"meta":113,"style":113},"language-typescript shiki shiki-themes github-light github-dark","export default defineNuxtModule\u003COptions>({\n  meta: {\n    name,\n    configKey,\n    compatibility: { nuxt: '>=3.0.0' },\n  },\n  defaults: { \u002F* ... *\u002F },\n  setup(options, nuxt) {\n    \u002F\u002F 1. Write a serialized config file into .nuxt\n    addTemplate({\n      filename: 'my-module-config.mjs',\n      getContents: () => `export default ${JSON.stringify(config)};\\n`,\n    });\n\n    \u002F\u002F 2. Register the runtime plugin\n    addPlugin({ src: resolver.resolve('.\u002Fruntime\u002Fplugin'), mode: 'all' });\n\n    \u002F\u002F 3. Augment Nuxt's types so $myModule shows up everywhere\n    const typesTemplate = addTemplate({\n      filename: 'types\u002Fmy-module.d.ts',\n      getContents: () => typeDeclarations,\n    });\n    nuxt.hook('prepare:types', ({ references }) => {\n      references.push({ path: typesTemplate.dst });\n    });\n  },\n});\n","typescript","",[29,115,116,142,148,154,160,173,179,191,213,219,228,240,283,289,296,302,328,333,339,356,366,378,383,411,423,428,433],{"__ignoreMap":113},[117,118,121,125,128,132,136,139],"span",{"class":119,"line":120},"line",1,[117,122,124],{"class":123},"szBVR","export",[117,126,127],{"class":123}," default",[117,129,131],{"class":130},"sScJk"," defineNuxtModule",[117,133,135],{"class":134},"sVt8B","\u003C",[117,137,138],{"class":130},"Options",[117,140,141],{"class":134},">({\n",[117,143,145],{"class":119,"line":144},2,[117,146,147],{"class":134},"  meta: {\n",[117,149,151],{"class":119,"line":150},3,[117,152,153],{"class":134},"    name,\n",[117,155,157],{"class":119,"line":156},4,[117,158,159],{"class":134},"    configKey,\n",[117,161,163,166,170],{"class":119,"line":162},5,[117,164,165],{"class":134},"    compatibility: { nuxt: ",[117,167,169],{"class":168},"sZZnC","'>=3.0.0'",[117,171,172],{"class":134}," },\n",[117,174,176],{"class":119,"line":175},6,[117,177,178],{"class":134},"  },\n",[117,180,182,185,189],{"class":119,"line":181},7,[117,183,184],{"class":134},"  defaults: { ",[117,186,188],{"class":187},"sJ8bj","\u002F* ... *\u002F",[117,190,172],{"class":134},[117,192,194,197,200,204,207,210],{"class":119,"line":193},8,[117,195,196],{"class":130},"  setup",[117,198,199],{"class":134},"(",[117,201,203],{"class":202},"s4XuR","options",[117,205,206],{"class":134},", ",[117,208,209],{"class":202},"nuxt",[117,211,212],{"class":134},") {\n",[117,214,216],{"class":119,"line":215},9,[117,217,218],{"class":187},"    \u002F\u002F 1. Write a serialized config file into .nuxt\n",[117,220,222,225],{"class":119,"line":221},10,[117,223,224],{"class":130},"    addTemplate",[117,226,227],{"class":134},"({\n",[117,229,231,234,237],{"class":119,"line":230},11,[117,232,233],{"class":134},"      filename: ",[117,235,236],{"class":168},"'my-module-config.mjs'",[117,238,239],{"class":134},",\n",[117,241,243,246,249,252,255,259,261,264,266,269,272,275,278,281],{"class":119,"line":242},12,[117,244,245],{"class":130},"      getContents",[117,247,248],{"class":134},": () ",[117,250,251],{"class":123},"=>",[117,253,254],{"class":168}," `export default ${",[117,256,258],{"class":257},"sj4cs","JSON",[117,260,17],{"class":168},[117,262,263],{"class":130},"stringify",[117,265,199],{"class":168},[117,267,268],{"class":134},"config",[117,270,271],{"class":168},")",[117,273,274],{"class":168},"};",[117,276,277],{"class":257},"\\n",[117,279,280],{"class":168},"`",[117,282,239],{"class":134},[117,284,286],{"class":119,"line":285},13,[117,287,288],{"class":134},"    });\n",[117,290,292],{"class":119,"line":291},14,[117,293,295],{"emptyLinePlaceholder":294},true,"\n",[117,297,299],{"class":119,"line":298},15,[117,300,301],{"class":187},"    \u002F\u002F 2. Register the runtime plugin\n",[117,303,305,308,311,314,316,319,322,325],{"class":119,"line":304},16,[117,306,307],{"class":130},"    addPlugin",[117,309,310],{"class":134},"({ src: resolver.",[117,312,313],{"class":130},"resolve",[117,315,199],{"class":134},[117,317,318],{"class":168},"'.\u002Fruntime\u002Fplugin'",[117,320,321],{"class":134},"), mode: ",[117,323,324],{"class":168},"'all'",[117,326,327],{"class":134}," });\n",[117,329,331],{"class":119,"line":330},17,[117,332,295],{"emptyLinePlaceholder":294},[117,334,336],{"class":119,"line":335},18,[117,337,338],{"class":187},"    \u002F\u002F 3. Augment Nuxt's types so $myModule shows up everywhere\n",[117,340,342,345,348,351,354],{"class":119,"line":341},19,[117,343,344],{"class":123},"    const",[117,346,347],{"class":257}," typesTemplate",[117,349,350],{"class":123}," =",[117,352,353],{"class":130}," addTemplate",[117,355,227],{"class":134},[117,357,359,361,364],{"class":119,"line":358},20,[117,360,233],{"class":134},[117,362,363],{"class":168},"'types\u002Fmy-module.d.ts'",[117,365,239],{"class":134},[117,367,369,371,373,375],{"class":119,"line":368},21,[117,370,245],{"class":130},[117,372,248],{"class":134},[117,374,251],{"class":123},[117,376,377],{"class":134}," typeDeclarations,\n",[117,379,381],{"class":119,"line":380},22,[117,382,288],{"class":134},[117,384,386,389,392,394,397,400,403,406,408],{"class":119,"line":385},23,[117,387,388],{"class":134},"    nuxt.",[117,390,391],{"class":130},"hook",[117,393,199],{"class":134},[117,395,396],{"class":168},"'prepare:types'",[117,398,399],{"class":134},", ({ ",[117,401,402],{"class":202},"references",[117,404,405],{"class":134}," }) ",[117,407,251],{"class":123},[117,409,410],{"class":134}," {\n",[117,412,414,417,420],{"class":119,"line":413},24,[117,415,416],{"class":134},"      references.",[117,418,419],{"class":130},"push",[117,421,422],{"class":134},"({ path: typesTemplate.dst });\n",[117,424,426],{"class":119,"line":425},25,[117,427,288],{"class":134},[117,429,431],{"class":119,"line":430},26,[117,432,178],{"class":134},[117,434,436],{"class":119,"line":435},27,[117,437,438],{"class":134},"});\n",[10,440,441,442,446,447,450,451,454],{},"The interesting part is ",[443,444,445],"em",{},"what I'm not doing",". I'm not passing the config object through provide\u002Finject at runtime, and I'm not importing user code directly from ",[29,448,449],{},"module.ts",". Everything flows through generated files in ",[29,452,453],{},".nuxt",". This has two benefits:",[456,457,458,461],"ol",{},[36,459,460],{},"The runtime plugin stays tiny. It just imports a plain JS object from a virtual path. No work at boot.",[36,462,463],{},"Tree-shaking works. If a feature isn't used, its template contents aren't referenced, and the bundle drops it.",[465,466,468,469,472],"h3",{"id":467},"virtual-modules-and-how-to-keep-tsc-happy","Virtual modules, and how to keep ",[29,470,471],{},"tsc"," happy",[10,474,475,476,479,480,483],{},"Generated templates don't exist on disk when the type-checker runs. Without extra work, ",[29,477,478],{},"import config from '#build\u002Fapi-provider-config.mjs'"," would be flagged as missing. Each package has a ",[29,481,482],{},"nuxt-virtual.d.ts"," declaring stubs:",[108,485,487],{"className":110,"code":486,"language":112,"meta":113,"style":113},"declare module '#build\u002Fapi-provider-config.mjs' {\n  const config: {\n    baseURL: string;\n    defaultTimeoutMs: number;\n    retry: { attempts: number; baseDelayMs: number };\n  };\n  export default config;\n}\n",[29,488,489,502,515,528,540,570,575,585],{"__ignoreMap":113},[117,490,491,494,497,500],{"class":119,"line":120},[117,492,493],{"class":123},"declare",[117,495,496],{"class":123}," module",[117,498,499],{"class":168}," '#build\u002Fapi-provider-config.mjs'",[117,501,410],{"class":134},[117,503,504,507,510,513],{"class":119,"line":144},[117,505,506],{"class":123},"  const",[117,508,509],{"class":257}," config",[117,511,512],{"class":123},":",[117,514,410],{"class":134},[117,516,517,520,522,525],{"class":119,"line":150},[117,518,519],{"class":202},"    baseURL",[117,521,512],{"class":123},[117,523,524],{"class":257}," string",[117,526,527],{"class":134},";\n",[117,529,530,533,535,538],{"class":119,"line":156},[117,531,532],{"class":202},"    defaultTimeoutMs",[117,534,512],{"class":123},[117,536,537],{"class":257}," number",[117,539,527],{"class":134},[117,541,542,545,547,550,553,555,557,560,563,565,567],{"class":119,"line":162},[117,543,544],{"class":202},"    retry",[117,546,512],{"class":123},[117,548,549],{"class":134}," { ",[117,551,552],{"class":202},"attempts",[117,554,512],{"class":123},[117,556,537],{"class":257},[117,558,559],{"class":134},"; ",[117,561,562],{"class":202},"baseDelayMs",[117,564,512],{"class":123},[117,566,537],{"class":257},[117,568,569],{"class":134}," };\n",[117,571,572],{"class":119,"line":175},[117,573,574],{"class":134},"  };\n",[117,576,577,580,582],{"class":119,"line":181},[117,578,579],{"class":123},"  export",[117,581,127],{"class":123},[117,583,584],{"class":134}," config;\n",[117,586,587],{"class":119,"line":193},[117,588,589],{"class":134},"}\n",[10,591,592,593,596],{},"Now ",[29,594,595],{},"tsc --noEmit"," passes, and editor autocomplete still works on fields of the generated config.",[22,598,600,603],{"id":599},"nuxt-api-provider-chainable-interceptors-and-two-transports",[29,601,602],{},"nuxt-api-provider",": chainable interceptors and two transports",[10,605,606],{},"The public surface of the API client is deliberately flat. You call it like a function:",[108,608,610],{"className":110,"code":609,"language":112,"meta":113,"style":113},"const users = await $apiProvider\u003CUser[]>('\u002Fusers', { method: 'GET' });\n",[29,611,612],{"__ignoreMap":113},[117,613,614,617,620,622,625,628,630,633,636,639,642,645],{"class":119,"line":120},[117,615,616],{"class":123},"const",[117,618,619],{"class":257}," users",[117,621,350],{"class":123},[117,623,624],{"class":123}," await",[117,626,627],{"class":130}," $apiProvider",[117,629,135],{"class":134},[117,631,632],{"class":130},"User",[117,634,635],{"class":134},"[]>(",[117,637,638],{"class":168},"'\u002Fusers'",[117,640,641],{"class":134},", { method: ",[117,643,644],{"class":168},"'GET'",[117,646,327],{"class":134},[10,648,649],{},"You add cross-cutting behavior through three chains:",[108,651,653],{"className":110,"code":652,"language":112,"meta":113,"style":113},"$apiProvider.useRequest((ctx) => {\n  ctx.headers.Authorization = `Bearer ${token}`;\n});\n\n$apiProvider.useResponse((ctx, response) => { \u002F* ... *\u002F });\n$apiProvider.useError((ctx, err) => { \u002F* ... *\u002F });\n",[29,654,655,676,695,699,703,729],{"__ignoreMap":113},[117,656,657,660,663,666,669,672,674],{"class":119,"line":120},[117,658,659],{"class":134},"$apiProvider.",[117,661,662],{"class":130},"useRequest",[117,664,665],{"class":134},"((",[117,667,668],{"class":202},"ctx",[117,670,671],{"class":134},") ",[117,673,251],{"class":123},[117,675,410],{"class":134},[117,677,678,681,684,687,690,693],{"class":119,"line":144},[117,679,680],{"class":134},"  ctx.headers.Authorization ",[117,682,683],{"class":123},"=",[117,685,686],{"class":168}," `Bearer ${",[117,688,689],{"class":134},"token",[117,691,692],{"class":168},"}`",[117,694,527],{"class":134},[117,696,697],{"class":119,"line":150},[117,698,438],{"class":134},[117,700,701],{"class":119,"line":156},[117,702,295],{"emptyLinePlaceholder":294},[117,704,705,707,710,712,714,716,719,721,723,725,727],{"class":119,"line":162},[117,706,659],{"class":134},[117,708,709],{"class":130},"useResponse",[117,711,665],{"class":134},[117,713,668],{"class":202},[117,715,206],{"class":134},[117,717,718],{"class":202},"response",[117,720,671],{"class":134},[117,722,251],{"class":123},[117,724,549],{"class":134},[117,726,188],{"class":187},[117,728,327],{"class":134},[117,730,731,733,736,738,740,742,745,747,749,751,753],{"class":119,"line":175},[117,732,659],{"class":134},[117,734,735],{"class":130},"useError",[117,737,665],{"class":134},[117,739,668],{"class":202},[117,741,206],{"class":134},[117,743,744],{"class":202},"err",[117,746,671],{"class":134},[117,748,251],{"class":123},[117,750,549],{"class":134},[117,752,188],{"class":187},[117,754,327],{"class":134},[10,756,757,758,761],{},"Each ",[29,759,760],{},"use*"," returns an unsubscribe function, which matters if you register interceptors from a component and want to clean up on unmount.",[465,763,765],{"id":764},"two-transports-one-api","Two transports, one API",[10,767,768,769,772,773,775,776,779,780,783,784,787],{},"Most requests go through ",[29,770,771],{},"fetch",". But ",[29,774,771],{}," doesn't expose upload progress. The ",[29,777,778],{},"ReadableStream"," side of the Request body is fine for streams but browsers don't give you byte-level ",[29,781,782],{},"progress"," events the way XHR does. So when a caller passes ",[29,785,786],{},"onRequestProgress",", the client swaps transports:",[108,789,791],{"className":110,"code":790,"language":112,"meta":113,"style":113},"const transport = ctx.options.onRequestProgress\n  ? createXhrFetch(ctx.options.onRequestProgress)\n  : defaultFetch;\n",[29,792,793,805,816],{"__ignoreMap":113},[117,794,795,797,800,802],{"class":119,"line":120},[117,796,616],{"class":123},[117,798,799],{"class":257}," transport",[117,801,350],{"class":123},[117,803,804],{"class":134}," ctx.options.onRequestProgress\n",[117,806,807,810,813],{"class":119,"line":144},[117,808,809],{"class":123},"  ?",[117,811,812],{"class":130}," createXhrFetch",[117,814,815],{"class":134},"(ctx.options.onRequestProgress)\n",[117,817,818,821],{"class":119,"line":150},[117,819,820],{"class":123},"  :",[117,822,823],{"class":134}," defaultFetch;\n",[10,825,826],{},"The XHR wrapper returns a Response-shaped object so the rest of the pipeline doesn't care how the bytes came back. That kind of \"one API, swap the engine under it\" has been my favorite pattern for two years. It keeps optional features optional without branching the whole code path.",[465,828,830],{"id":829},"interceptors-by-path-not-by-function","Interceptors by path, not by function",[10,832,833],{},"The module options look like this:",[108,835,837],{"className":110,"code":836,"language":112,"meta":113,"style":113},"{\n  baseURL: 'https:\u002F\u002Fapi.example.com',\n  onRequestPath:  '~\u002Fapi\u002Fon-request.ts',\n  onSuccessPath:  '~\u002Fapi\u002Fon-response.ts',\n  onErrorPath:    '~\u002Fapi\u002Fon-error.ts',\n}\n",[29,838,839,844,857,870,882,895],{"__ignoreMap":113},[117,840,841],{"class":119,"line":120},[117,842,843],{"class":134},"{\n",[117,845,846,849,852,855],{"class":119,"line":144},[117,847,848],{"class":130},"  baseURL",[117,850,851],{"class":134},": ",[117,853,854],{"class":168},"'https:\u002F\u002Fapi.example.com'",[117,856,239],{"class":134},[117,858,859,862,865,868],{"class":119,"line":150},[117,860,861],{"class":130},"  onRequestPath",[117,863,864],{"class":134},":  ",[117,866,867],{"class":168},"'~\u002Fapi\u002Fon-request.ts'",[117,869,239],{"class":134},[117,871,872,875,877,880],{"class":119,"line":156},[117,873,874],{"class":130},"  onSuccessPath",[117,876,864],{"class":134},[117,878,879],{"class":168},"'~\u002Fapi\u002Fon-response.ts'",[117,881,239],{"class":134},[117,883,884,887,890,893],{"class":119,"line":162},[117,885,886],{"class":130},"  onErrorPath",[117,888,889],{"class":134},":    ",[117,891,892],{"class":168},"'~\u002Fapi\u002Fon-error.ts'",[117,894,239],{"class":134},[117,896,897],{"class":119,"line":175},[117,898,589],{"class":134},[10,900,901,902,905],{},"Interceptors are resolved as ",[443,903,904],{},"file paths",", not inline functions. The generated template dynamically imports them, and the runtime plugin wires whatever's exported into the chain. Two reasons:",[33,907,908,918],{},[36,909,910,913,914,917],{},[14,911,912],{},"No circular config."," Users often want to import types from the api-provider module inside their interceptor. If the interceptor lived inside ",[29,915,916],{},"nuxt.config.ts",", the config file would depend on the module it's configuring.",[36,919,920,923],{},[14,921,922],{},"Code-splitting."," The interceptor becomes its own chunk, which matters on cold loads.",[465,925,927,928],{"id":926},"error-branding-without-instanceof","Error branding without ",[29,929,930],{},"instanceof",[10,932,933,935],{},[29,934,930],{}," fails the moment you have two copies of the same class. This happens with duplicated deps, iframes, web workers, or pnpm hoisting quirks. The client's error type uses a Symbol brand instead:",[108,937,939],{"className":110,"code":938,"language":112,"meta":113,"style":113},"const API_ERROR_BRAND: unique symbol = Symbol.for(\n  '@alikhalilll\u002Fnuxt-api-provider.ApiError'\n);\n\nexport class ApiError extends Error {\n  readonly [API_ERROR_BRAND] = true;\n\n  static is(e: unknown): e is ApiError {\n    return typeof e === 'object' && e !== null && API_ERROR_BRAND in e;\n  }\n}\n",[29,940,941,967,972,977,981,999,1020,1024,1055,1093,1098],{"__ignoreMap":113},[117,942,943,945,948,950,953,956,958,961,964],{"class":119,"line":120},[117,944,616],{"class":123},[117,946,947],{"class":257}," API_ERROR_BRAND",[117,949,512],{"class":123},[117,951,952],{"class":130}," unique",[117,954,955],{"class":257}," symbol",[117,957,350],{"class":123},[117,959,960],{"class":134}," Symbol.",[117,962,963],{"class":130},"for",[117,965,966],{"class":134},"(\n",[117,968,969],{"class":119,"line":144},[117,970,971],{"class":168},"  '@alikhalilll\u002Fnuxt-api-provider.ApiError'\n",[117,973,974],{"class":119,"line":150},[117,975,976],{"class":134},");\n",[117,978,979],{"class":119,"line":156},[117,980,295],{"emptyLinePlaceholder":294},[117,982,983,985,988,991,994,997],{"class":119,"line":162},[117,984,124],{"class":123},[117,986,987],{"class":123}," class",[117,989,990],{"class":130}," ApiError",[117,992,993],{"class":123}," extends",[117,995,996],{"class":130}," Error",[117,998,410],{"class":134},[117,1000,1001,1004,1007,1010,1013,1015,1018],{"class":119,"line":175},[117,1002,1003],{"class":123},"  readonly",[117,1005,1006],{"class":134}," [",[117,1008,1009],{"class":257},"API_ERROR_BRAND",[117,1011,1012],{"class":134},"] ",[117,1014,683],{"class":123},[117,1016,1017],{"class":257}," true",[117,1019,527],{"class":134},[117,1021,1022],{"class":119,"line":181},[117,1023,295],{"emptyLinePlaceholder":294},[117,1025,1026,1029,1032,1034,1037,1039,1042,1044,1046,1049,1051,1053],{"class":119,"line":193},[117,1027,1028],{"class":123},"  static",[117,1030,1031],{"class":130}," is",[117,1033,199],{"class":134},[117,1035,1036],{"class":202},"e",[117,1038,512],{"class":123},[117,1040,1041],{"class":257}," unknown",[117,1043,271],{"class":134},[117,1045,512],{"class":123},[117,1047,1048],{"class":202}," e",[117,1050,1031],{"class":123},[117,1052,990],{"class":130},[117,1054,410],{"class":134},[117,1056,1057,1060,1063,1066,1069,1072,1075,1077,1080,1083,1085,1087,1090],{"class":119,"line":215},[117,1058,1059],{"class":123},"    return",[117,1061,1062],{"class":123}," typeof",[117,1064,1065],{"class":134}," e ",[117,1067,1068],{"class":123},"===",[117,1070,1071],{"class":168}," 'object'",[117,1073,1074],{"class":123}," &&",[117,1076,1065],{"class":134},[117,1078,1079],{"class":123},"!==",[117,1081,1082],{"class":257}," null",[117,1084,1074],{"class":123},[117,1086,947],{"class":257},[117,1088,1089],{"class":123}," in",[117,1091,1092],{"class":134}," e;\n",[117,1094,1095],{"class":119,"line":221},[117,1096,1097],{"class":134},"  }\n",[117,1099,1100],{"class":119,"line":230},[117,1101,589],{"class":134},[10,1103,1104,1107,1108,1111,1112,1115,1116,1119],{},[29,1105,1106],{},"Symbol.for"," gives you the ",[443,1109,1110],{},"same"," symbol across module copies. ",[29,1113,1114],{},"ApiError.is(err)"," works where ",[29,1117,1118],{},"err instanceof ApiError"," doesn't. This has saved me on every project that ever crossed a realm boundary.",[22,1121,1123,1126],{"id":1122},"nuxt-auto-middleware-compile-time-regex-runtime-dispatch",[29,1124,1125],{},"nuxt-auto-middleware",": compile-time regex, runtime dispatch",[10,1128,1129],{},"The module takes rules like this:",[108,1131,1133],{"className":110,"code":1132,"language":112,"meta":113,"style":113},"autoMiddleware: {\n  groups: {\n    adminOnly: ['auth', 'require-admin'],\n  },\n  rules: [\n    { layouts: ['admin-*'], middlewares: ['@adminOnly'] },\n    { layouts: [\u002F^workspace\\\u002F.*\u002F], middlewares: ['auth', 'workspace'] },\n  ],\n}\n",[29,1134,1135,1143,1150,1169,1173,1181,1198,1234,1239],{"__ignoreMap":113},[117,1136,1137,1140],{"class":119,"line":120},[117,1138,1139],{"class":130},"autoMiddleware",[117,1141,1142],{"class":134},": {\n",[117,1144,1145,1148],{"class":119,"line":144},[117,1146,1147],{"class":130},"  groups",[117,1149,1142],{"class":134},[117,1151,1152,1155,1158,1161,1163,1166],{"class":119,"line":150},[117,1153,1154],{"class":130},"    adminOnly",[117,1156,1157],{"class":134},": [",[117,1159,1160],{"class":168},"'auth'",[117,1162,206],{"class":134},[117,1164,1165],{"class":168},"'require-admin'",[117,1167,1168],{"class":134},"],\n",[117,1170,1171],{"class":119,"line":156},[117,1172,178],{"class":134},[117,1174,1175,1178],{"class":119,"line":162},[117,1176,1177],{"class":130},"  rules",[117,1179,1180],{"class":134},": [\n",[117,1182,1183,1186,1189,1192,1195],{"class":119,"line":175},[117,1184,1185],{"class":134},"    { layouts: [",[117,1187,1188],{"class":168},"'admin-*'",[117,1190,1191],{"class":134},"], middlewares: [",[117,1193,1194],{"class":168},"'@adminOnly'",[117,1196,1197],{"class":134},"] },\n",[117,1199,1200,1202,1205,1208,1212,1216,1218,1221,1223,1225,1227,1229,1232],{"class":119,"line":181},[117,1201,1185],{"class":134},[117,1203,1204],{"class":168},"\u002F",[117,1206,1207],{"class":123},"^",[117,1209,1211],{"class":1210},"sA_wV","workspace",[117,1213,1215],{"class":1214},"snhLl","\\\u002F",[117,1217,17],{"class":257},[117,1219,1220],{"class":123},"*",[117,1222,1204],{"class":168},[117,1224,1191],{"class":134},[117,1226,1160],{"class":168},[117,1228,206],{"class":134},[117,1230,1231],{"class":168},"'workspace'",[117,1233,1197],{"class":134},[117,1235,1236],{"class":119,"line":193},[117,1237,1238],{"class":134},"  ],\n",[117,1240,1241],{"class":119,"line":215},[117,1242,589],{"class":134},[10,1244,1245,1246,1249],{},"At module setup time, each glob gets compiled to a RegExp. Each group reference gets expanded. The resulting rules get ",[443,1247,1248],{},"serialized"," into a generated template:",[108,1251,1253],{"className":110,"code":1252,"language":112,"meta":113,"style":113},"export const rules = [\n  { patterns: ['^admin-.*$'], middlewares: ['auth', 'require-admin'] },\n  { patterns: ['^workspace\\\\\u002F.*'], middlewares: ['auth', 'workspace'] },\n];\n",[29,1254,1255,1270,1288,1311],{"__ignoreMap":113},[117,1256,1257,1259,1262,1265,1267],{"class":119,"line":120},[117,1258,124],{"class":123},[117,1260,1261],{"class":123}," const",[117,1263,1264],{"class":257}," rules",[117,1266,350],{"class":123},[117,1268,1269],{"class":134}," [\n",[117,1271,1272,1275,1278,1280,1282,1284,1286],{"class":119,"line":144},[117,1273,1274],{"class":134},"  { patterns: [",[117,1276,1277],{"class":168},"'^admin-.*$'",[117,1279,1191],{"class":134},[117,1281,1160],{"class":168},[117,1283,206],{"class":134},[117,1285,1165],{"class":168},[117,1287,1197],{"class":134},[117,1289,1290,1292,1295,1298,1301,1303,1305,1307,1309],{"class":119,"line":150},[117,1291,1274],{"class":134},[117,1293,1294],{"class":168},"'^workspace",[117,1296,1297],{"class":257},"\\\\",[117,1299,1300],{"class":168},"\u002F.*'",[117,1302,1191],{"class":134},[117,1304,1160],{"class":168},[117,1306,206],{"class":134},[117,1308,1231],{"class":168},[117,1310,1197],{"class":134},[117,1312,1313],{"class":119,"line":156},[117,1314,1315],{"class":134},"];\n",[10,1317,1318,1319,1322],{},"At runtime, the plugin rehydrates the patterns with ",[29,1320,1321],{},"new RegExp(source)"," and matches against the current layout. The client never sees a glob parser; it's been pre-compiled away. On a typical app this saves a few KB, but more importantly it means adding more rules doesn't cost more bundle size beyond the rule strings themselves.",[22,1324,1326,1329],{"id":1325},"nuxt-crypto-the-lru-that-caches-promises",[29,1327,1328],{},"nuxt-crypto",": the LRU that caches promises",[10,1331,1332],{},"The crypto service is the part I'm happiest with. It wraps Web Crypto's AES-GCM + PBKDF2 primitives with a clean encrypt\u002Fdecrypt API. The trick is the key cache.",[10,1334,1335,1336,1339],{},"PBKDF2 with 100,000 iterations is slow on purpose; that's the whole point. But when your UI tries to decrypt three fields from IndexedDB in parallel, doing three separate key derivations is both wasteful and slow. The cache fixes that, but the detail that actually matters is ",[443,1337,1338],{},"what"," it caches:",[108,1341,1343],{"className":110,"code":1342,"language":112,"meta":113,"style":113},"const getDerivedKey = async (salt, fingerprint?) => {\n  const key = KeyCache.key(salt, iterations, fingerprint);\n  const cached = cache.get(key);\n  if (cached) return cached;\n\n  \u002F\u002F Cache the promise, not the settled key\n  const pending = algorithm.deriveKey({\n    subtle,\n    passphrase,\n    fingerprint,\n    salt,\n    iterations,\n  });\n  cache.set(key, pending);\n  return pending;\n};\n",[29,1344,1345,1377,1395,1413,1427,1431,1436,1453,1458,1463,1468,1473,1478,1483,1494,1502],{"__ignoreMap":113},[117,1346,1347,1349,1352,1354,1357,1360,1363,1365,1368,1371,1373,1375],{"class":119,"line":120},[117,1348,616],{"class":123},[117,1350,1351],{"class":130}," getDerivedKey",[117,1353,350],{"class":123},[117,1355,1356],{"class":123}," async",[117,1358,1359],{"class":134}," (",[117,1361,1362],{"class":202},"salt",[117,1364,206],{"class":134},[117,1366,1367],{"class":202},"fingerprint",[117,1369,1370],{"class":123},"?",[117,1372,671],{"class":134},[117,1374,251],{"class":123},[117,1376,410],{"class":134},[117,1378,1379,1381,1384,1386,1389,1392],{"class":119,"line":144},[117,1380,506],{"class":123},[117,1382,1383],{"class":257}," key",[117,1385,350],{"class":123},[117,1387,1388],{"class":134}," KeyCache.",[117,1390,1391],{"class":130},"key",[117,1393,1394],{"class":134},"(salt, iterations, fingerprint);\n",[117,1396,1397,1399,1402,1404,1407,1410],{"class":119,"line":150},[117,1398,506],{"class":123},[117,1400,1401],{"class":257}," cached",[117,1403,350],{"class":123},[117,1405,1406],{"class":134}," cache.",[117,1408,1409],{"class":130},"get",[117,1411,1412],{"class":134},"(key);\n",[117,1414,1415,1418,1421,1424],{"class":119,"line":156},[117,1416,1417],{"class":123},"  if",[117,1419,1420],{"class":134}," (cached) ",[117,1422,1423],{"class":123},"return",[117,1425,1426],{"class":134}," cached;\n",[117,1428,1429],{"class":119,"line":162},[117,1430,295],{"emptyLinePlaceholder":294},[117,1432,1433],{"class":119,"line":175},[117,1434,1435],{"class":187},"  \u002F\u002F Cache the promise, not the settled key\n",[117,1437,1438,1440,1443,1445,1448,1451],{"class":119,"line":181},[117,1439,506],{"class":123},[117,1441,1442],{"class":257}," pending",[117,1444,350],{"class":123},[117,1446,1447],{"class":134}," algorithm.",[117,1449,1450],{"class":130},"deriveKey",[117,1452,227],{"class":134},[117,1454,1455],{"class":119,"line":193},[117,1456,1457],{"class":134},"    subtle,\n",[117,1459,1460],{"class":119,"line":215},[117,1461,1462],{"class":134},"    passphrase,\n",[117,1464,1465],{"class":119,"line":221},[117,1466,1467],{"class":134},"    fingerprint,\n",[117,1469,1470],{"class":119,"line":230},[117,1471,1472],{"class":134},"    salt,\n",[117,1474,1475],{"class":119,"line":242},[117,1476,1477],{"class":134},"    iterations,\n",[117,1479,1480],{"class":119,"line":285},[117,1481,1482],{"class":134},"  });\n",[117,1484,1485,1488,1491],{"class":119,"line":291},[117,1486,1487],{"class":134},"  cache.",[117,1489,1490],{"class":130},"set",[117,1492,1493],{"class":134},"(key, pending);\n",[117,1495,1496,1499],{"class":119,"line":298},[117,1497,1498],{"class":123},"  return",[117,1500,1501],{"class":134}," pending;\n",[117,1503,1504],{"class":119,"line":304},[117,1505,1506],{"class":134},"};\n",[10,1508,1509,1510,1513,1514,1517,1518,1521,1522,1524],{},"The cache holds ",[29,1511,1512],{},"Promise\u003CCryptoKey>",", not ",[29,1515,1516],{},"CryptoKey",". If three ",[29,1519,1520],{},"decrypt()"," calls arrive in the same tick with the same salt, they all await the ",[443,1523,1110],{}," pending promise. PBKDF2 runs once. The naive version (cache the settled key) leaves a window where two calls both see \"not cached\" and start duplicate work.",[10,1526,1527,1528,1531],{},"The LRU itself uses the fact that JavaScript ",[29,1529,1530],{},"Map"," preserves insertion order:",[108,1533,1535],{"className":110,"code":1534,"language":112,"meta":113,"style":113},"get(key: string): Promise\u003CCryptoKey> | undefined {\n  const value = this.map.get(key);\n  if (!value) return undefined;\n  this.map.delete(key);   \u002F\u002F delete + re-insert moves to newest\n  this.map.set(key, value);\n  return value;\n}\n",[29,1536,1537,1562,1581,1599,1615,1626,1633],{"__ignoreMap":113},[117,1538,1539,1541,1544,1547,1549,1551,1554,1557,1560],{"class":119,"line":120},[117,1540,1409],{"class":130},[117,1542,1543],{"class":134},"(key: string): ",[117,1545,1546],{"class":257},"Promise",[117,1548,135],{"class":123},[117,1550,1516],{"class":134},[117,1552,1553],{"class":123},">",[117,1555,1556],{"class":123}," |",[117,1558,1559],{"class":257}," undefined",[117,1561,410],{"class":134},[117,1563,1564,1566,1569,1571,1574,1577,1579],{"class":119,"line":144},[117,1565,506],{"class":123},[117,1567,1568],{"class":257}," value",[117,1570,350],{"class":123},[117,1572,1573],{"class":257}," this",[117,1575,1576],{"class":134},".map.",[117,1578,1409],{"class":130},[117,1580,1412],{"class":134},[117,1582,1583,1585,1587,1590,1593,1595,1597],{"class":119,"line":150},[117,1584,1417],{"class":123},[117,1586,1359],{"class":134},[117,1588,1589],{"class":123},"!",[117,1591,1592],{"class":134},"value) ",[117,1594,1423],{"class":123},[117,1596,1559],{"class":257},[117,1598,527],{"class":134},[117,1600,1601,1604,1606,1609,1612],{"class":119,"line":156},[117,1602,1603],{"class":257},"  this",[117,1605,1576],{"class":134},[117,1607,1608],{"class":130},"delete",[117,1610,1611],{"class":134},"(key);   ",[117,1613,1614],{"class":187},"\u002F\u002F delete + re-insert moves to newest\n",[117,1616,1617,1619,1621,1623],{"class":119,"line":162},[117,1618,1603],{"class":257},[117,1620,1576],{"class":134},[117,1622,1490],{"class":130},[117,1624,1625],{"class":134},"(key, value);\n",[117,1627,1628,1630],{"class":119,"line":175},[117,1629,1498],{"class":123},[117,1631,1632],{"class":134}," value;\n",[117,1634,1635],{"class":119,"line":181},[117,1636,589],{"class":134},[10,1638,1639,1640,1643],{},"Eviction is then just ",[29,1641,1642],{},"this.map.keys().next().value",": the oldest.",[465,1645,1647],{"id":1646},"versioned-payloads","Versioned payloads",[10,1649,1650],{},"Encrypt output carries a version byte:",[108,1652,1657],{"className":1653,"code":1655,"language":1656},[1654],"language-text","[version: 1B] [salt: 16B] [iv: 12B] [ciphertext: ...]\n","text",[29,1658,1655],{"__ignoreMap":113},[10,1660,1661],{},"On decrypt, mismatched versions fail early. This is the kind of thing you add before you need it. The day I want to rotate from AES-GCM to something post-quantum, the old payloads will still decrypt through the old algorithm while new ones use the new one. Without a version byte you'd be stuck guessing.",[22,1663,1665],{"id":1664},"the-docs-site-as-a-playground-for-the-modules","The docs site as a playground for the modules",[10,1667,1668,1671,1672,1675],{},[29,1669,1670],{},"apps\u002Fdocs"," uses all three modules. That sounds obvious, but it's load-bearing: the docs are ",[443,1673,1674],{},"how I notice breakage"," before users do. If an update to api-provider breaks the doc site's JSON Placeholder demo, CI fails on the build step. No test suite needed for that particular class of regression.",[10,1677,1678],{},"Other things that earned their keep:",[33,1680,1681,1693,1707],{},[36,1682,1683,1688,1689,1692],{},[14,1684,1685],{},[29,1686,1687],{},"@tailwindcss\u002Fvite",": no PostCSS, no ",[29,1690,1691],{},"tailwind.config.js",", just a Vite plugin. One less config file in my life.",[36,1694,1695,1698,1699,1702,1703,1706],{},[14,1696,1697],{},"Static prerender to GitHub Pages",": Nitro's ",[29,1700,1701],{},"github-pages"," preset gives you a ",[29,1704,1705],{},"\u002F.output\u002Fpublic\u002F"," that Actions uploads straight to Pages. No servers, no cache invalidation, no surprises.",[36,1708,1709,1714],{},[14,1710,1711],{},[29,1712,1713],{},"NUXT_PUBLIC_CF_ANALYTICS_TOKEN",": optional Cloudflare Web Analytics. No cookies, no banner, no opinion.",[22,1716,1718],{"id":1717},"release-changesets-all-the-way-down","Release: Changesets all the way down",[10,1720,1721,1722,1725,1726,1729],{},"Every PR that changes a package includes a ",[29,1723,1724],{},".changeset\u002F\u003Cid>.md"," describing the bump and changelog entry. On master, a GitHub Action runs ",[29,1727,1728],{},"changesets version"," and either:",[456,1731,1732,1735],{},[36,1733,1734],{},"Opens a \"Version Packages\" PR with the bumped versions, or",[36,1736,1737],{},"Publishes to npm if versions are already bumped.",[10,1739,1740,1741,1744,1745,1748,1749,1752],{},"This is the least-effort setup I've found that gives honest changelogs, semver discipline, and no-touch npm publishing. The CI pipeline itself is three files: ",[29,1742,1743],{},"ci.yml"," (lint + typecheck + matrix build), ",[29,1746,1747],{},"release.yml"," (changesets), and ",[29,1750,1751],{},"commitlint.yml"," (conventional commits on PRs).",[22,1754,1756],{"id":1755},"the-parts-id-reuse-tomorrow","The parts I'd reuse tomorrow",[10,1758,1759],{},"If I were starting a new Nuxt module from scratch, I'd lift these patterns without thinking:",[33,1761,1762,1772,1782,1788,1794],{},[36,1763,1764,1771],{},[14,1765,1766,1767,1770],{},"Serialize config into a generated ",[29,1768,1769],{},".mjs"," file."," Avoids re-evaluating at runtime, and the runtime plugin stays 15 lines.",[36,1773,1774,1781],{},[14,1775,1776,1777,1780],{},"Virtual-module ",[29,1778,1779],{},".d.ts"," stubs."," Offline typecheck without running the Nuxt prepare step.",[36,1783,1784,1787],{},[14,1785,1786],{},"Symbol-branded errors."," Free for low-traffic modules, life-saving for the ones that cross realms.",[36,1789,1790,1793],{},[14,1791,1792],{},"Promise-keyed caches."," Any time the underlying operation is async and expensive, cache the promise, not the result.",[36,1795,1796,1799],{},[14,1797,1798],{},"Framework-agnostic core + thin Nuxt wrapper."," Makes the code easier to test, easier to reuse, and easier to delete.",[10,1801,1802,1803,1810],{},"The repo is MIT-licensed and lives on ",[1804,1805,1809],"a",{"href":1806,"rel":1807},"https:\u002F\u002Fgithub.com\u002Falikhalilll",[1808],"nofollow","GitHub",". If you spot something that could be better, open an issue. I'd genuinely rather be corrected than comfortable.",[1812,1813,1814],"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 .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}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 .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}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":113,"searchDepth":144,"depth":144,"links":1816},[1817,1818,1822,1829,1831,1835,1836,1837],{"id":24,"depth":144,"text":25},{"id":102,"depth":144,"text":103,"children":1819},[1820],{"id":467,"depth":150,"text":1821},"Virtual modules, and how to keep tsc happy",{"id":599,"depth":144,"text":1823,"children":1824},"nuxt-api-provider: chainable interceptors and two transports",[1825,1826,1827],{"id":764,"depth":150,"text":765},{"id":829,"depth":150,"text":830},{"id":926,"depth":150,"text":1828},"Error branding without instanceof",{"id":1122,"depth":144,"text":1830},"nuxt-auto-middleware: compile-time regex, runtime dispatch",{"id":1325,"depth":144,"text":1832,"children":1833},"nuxt-crypto: the LRU that caches promises",[1834],{"id":1646,"depth":150,"text":1647},{"id":1664,"depth":144,"text":1665},{"id":1717,"depth":144,"text":1718},{"id":1755,"depth":144,"text":1756},"2026-01-12","Three Nuxt 4 modules in a pnpm monorepo. A typed fetch client with upload progress, AES-GCM and PBKDF2 crypto, and layout-scoped middleware.","ثلاث موديولات Nuxt 4 داخل monorepo مبني بـ pnpm. عميل fetch مكتوب بأنواع صارمة يدعم تقدّم الرفع، وتشفير AES-GCM مع PBKDF2، وطبقة middleware مربوطة بالـ layouts.",false,"md",null,[1845,1846,1847,1848,1849,1850,1851,1852,1853,1854,1855,1856],"Nuxt 4","Nuxt modules","TypeScript","pnpm monorepo","fetch client","interceptors","upload progress","AES-GCM","PBKDF2","Web Crypto","route middleware","Changesets","en",{},"\u002Fwrites\u002Fbuilding-ali-nuxt-toolkit",{"title":5,"description":1839},"writes\u002Fbuilding-ali-nuxt-toolkit","بناء ali-nuxt-toolkit، جولة داخل التفاصيل","Imy2c4cxZ1y_-_8kGo-d99wxcoSFFI0jHuoVychEQMc",[1865,3639],{"id":1866,"title":1867,"body":1868,"date":3621,"description":3622,"description_ar":3623,"draft":1841,"extension":1842,"image":1843,"keywords":3624,"lang":1857,"meta":3633,"navigation":294,"path":3634,"seo":3635,"stem":3636,"title_ar":3637,"updatedAt":1843,"__hash__":3638},"writes\u002Fwrites\u002Finside-a-skeleton-self-generating-skeleton-loaders.md","Inside @alikhalilll\u002Fa-skeleton, self-generating skeleton loaders",{"type":7,"value":1869,"toc":3610},[1870,1873,1879,1882,1886,1889,1894,1944,1949,1980,2008,2015,2029,2032,2036,2043,2064,2071,2082,2085,2126,2129,2216,2244,2250,2254,2261,2306,2331,2337,2340,2344,2354,2737,2774,2777,2845,2848,2965,2968,2991,2994,2998,3001,3007,3044,3063,3080,3090,3094,3142,3145,3152,3228,3241,3245,3288,3294,3305,3394,3401,3507,3529,3532,3536,3555,3573,3580,3584,3595,3601,3604,3607],[10,1871,1872],{},"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,1874,1875,1878],{},[29,1876,1877],{},"@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,1880,1881],{},"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.",[22,1883,1885],{"id":1884},"the-contract-keep-tags-unconditional-gate-content","The contract: keep tags unconditional, gate content",[10,1887,1888],{},"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,1890,1891],{},[14,1892,1893],{},"Wrong:",[108,1895,1899],{"className":1896,"code":1897,"language":1898,"meta":113,"style":113},"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",[29,1900,1901,1925,1930,1935],{"__ignoreMap":113},[117,1902,1903,1905,1909,1912,1914,1917,1920,1922],{"class":119,"line":120},[117,1904,135],{"class":134},[117,1906,1908],{"class":1907},"s9eBZ","div",[117,1910,1911],{"class":123}," v-if",[117,1913,683],{"class":134},[117,1915,1916],{"class":168},"\"",[117,1918,1919],{"class":134},"user",[117,1921,1916],{"class":168},[117,1923,1924],{"class":134},">\n",[117,1926,1927],{"class":119,"line":144},[117,1928,1929],{"class":134},"  \u003Ch3>{{ user.name }}\u003C\u002Fh3>\n",[117,1931,1932],{"class":119,"line":150},[117,1933,1934],{"class":134},"  \u003Cp>{{ user.bio }}\u003C\u002Fp>\n",[117,1936,1937,1940,1942],{"class":119,"line":156},[117,1938,1939],{"class":134},"\u003C\u002F",[117,1941,1908],{"class":1907},[117,1943,1924],{"class":134},[10,1945,1946],{},[14,1947,1948],{},"Right:",[108,1950,1952],{"className":1896,"code":1951,"language":1898,"meta":113,"style":113},"\u003Cdiv>\n  \u003Ch3>{{ user?.name }}\u003C\u002Fh3>\n  \u003Cp>{{ user?.bio }}\u003C\u002Fp>\n\u003C\u002Fdiv>\n",[29,1953,1954,1962,1967,1972],{"__ignoreMap":113},[117,1955,1956,1958,1960],{"class":119,"line":120},[117,1957,135],{"class":134},[117,1959,1908],{"class":1907},[117,1961,1924],{"class":134},[117,1963,1964],{"class":119,"line":144},[117,1965,1966],{"class":134},"  \u003Ch3>{{ user?.name }}\u003C\u002Fh3>\n",[117,1968,1969],{"class":119,"line":150},[117,1970,1971],{"class":134},"  \u003Cp>{{ user?.bio }}\u003C\u002Fp>\n",[117,1973,1974,1976,1978],{"class":119,"line":156},[117,1975,1939],{"class":134},[117,1977,1908],{"class":1907},[117,1979,1924],{"class":134},[10,1981,1982,1983,1986,1987,1990,1991,1993,1994,1997,1998,206,2001,206,2004,2007],{},"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 ",[29,1984,1985],{},"v-if=\"user\"",", the walker sees a comment node during loading and an ",[29,1988,1989],{},"\u003Ch3>"," after, and it has no way to predict the placeholder geometry. With the optional-chained interpolation, the ",[29,1992,1989],{}," is always there; its text is empty during loading, and the walker classifies empty text-owner tags (",[29,1995,1996],{},"\u003Ch1>","–",[29,1999,2000],{},"\u003Ch6>",[29,2002,2003],{},"\u003Cp>",[29,2005,2006],{},"\u003Cspan>",", etc.) as shimmer text bars at the heading's natural rendered height.",[10,2009,2010,2011,2014],{},"Tags are the schema. Content is the payload. You can swap the payload to ",[29,2012,2013],{},"null"," without the schema noticing. That single discipline is what unlocks the rest.",[10,2016,2017,2018,2021,2022,2024,2025,2028],{},"The other half of the contract is the ",[29,2019,2020],{},"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 ",[29,2023,2020],{}," and the walker treats it as invisible; the real element renders verbatim through both states. The corollary, ",[29,2026,2027],{},"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,2030,2031],{},"Two rules, two attributes. That's the whole authoring surface you ever have to learn.",[22,2033,2035],{"id":2034},"clone-mode-snapshot-the-final-paint-replay-the-pixels","Clone mode: snapshot the final paint, replay the pixels",[10,2037,2038,2039,2042],{},"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 ",[29,2040,2041],{},"rounded-*"," prefix. They guess.",[10,2044,2045,2046,2049,2050,2053,2054,2057,2058,2060,2061,2063],{},"Clone mode stops guessing. It mounts the slot off-screen in a ",[29,2047,2048],{},"visibility: hidden"," capture host, lets the browser do its full layout and style resolution, and then reads the ",[14,2051,2052],{},"final computed result"," for every element via ",[29,2055,2056],{},"getComputedStyle()",". Whatever your styling system is (Tailwind utilities, scoped styles with hashed class names, CSS-in-JS at runtime, DaisyUI semantic tokens, inline ",[29,2059,1812],{}," attributes set by a parent), by the time ",[29,2062,2056],{}," runs, all of it has collapsed into one canonical answer per property. The engine snapshots that answer and replays it.",[10,2065,2066,2067,2070],{},"The captured surface is comprehensive. Per-edge borders (so a card with a different ",[29,2068,2069],{},"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,2072,2073,2074,2077,2078,2081],{},"Text is the part that's easy to get wrong, and the part ",[29,2075,2076],{},"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 ",[29,2079,2080],{},"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,2083,2084],{},"The wrapper API for all of this is one prop:",[108,2086,2088],{"className":1896,"code":2087,"language":1898,"meta":113,"style":113},"\u003CASkeleton :loading=\"loading\">\n  \u003CSomeRichComponent \u002F>\n\u003C\u002FASkeleton>\n",[29,2089,2090,2113,2118],{"__ignoreMap":113},[117,2091,2092,2094,2097,2100,2103,2105,2107,2109,2111],{"class":119,"line":120},[117,2093,135],{"class":134},[117,2095,2096],{"class":1907},"ASkeleton",[117,2098,2099],{"class":134}," :",[117,2101,2102],{"class":130},"loading",[117,2104,683],{"class":134},[117,2106,1916],{"class":168},[117,2108,2102],{"class":134},[117,2110,1916],{"class":168},[117,2112,1924],{"class":134},[117,2114,2115],{"class":119,"line":144},[117,2116,2117],{"class":134},"  \u003CSomeRichComponent \u002F>\n",[117,2119,2120,2122,2124],{"class":119,"line":150},[117,2121,1939],{"class":134},[117,2123,2096],{"class":1907},[117,2125,1924],{"class":134},[10,2127,2128],{},"If you want to roll your own capture \u002F replay flow, the underlying function is a public export:",[108,2130,2134],{"className":2131,"code":2132,"language":2133,"meta":113,"style":113},"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",[29,2135,2136,2158,2162,2182,2192,2202,2212],{"__ignoreMap":113},[117,2137,2138,2141,2144,2147,2150,2153,2156],{"class":119,"line":120},[117,2139,2140],{"class":123},"import",[117,2142,2143],{"class":134}," { captureSnapshot, ",[117,2145,2146],{"class":123},"type",[117,2148,2149],{"class":134}," CaptureSnapshot } ",[117,2151,2152],{"class":123},"from",[117,2154,2155],{"class":168}," '@alikhalilll\u002Fa-skeleton'",[117,2157,527],{"class":134},[117,2159,2160],{"class":119,"line":144},[117,2161,295],{"emptyLinePlaceholder":294},[117,2163,2164,2166,2169,2171,2174,2176,2179],{"class":119,"line":150},[117,2165,616],{"class":123},[117,2167,2168],{"class":257}," snap",[117,2170,512],{"class":123},[117,2172,2173],{"class":130}," CaptureSnapshot",[117,2175,350],{"class":123},[117,2177,2178],{"class":130}," captureSnapshot",[117,2180,2181],{"class":134},"(myRootEl, {\n",[117,2183,2184,2187,2190],{"class":119,"line":156},[117,2185,2186],{"class":134},"  maxDepth: ",[117,2188,2189],{"class":257},"12",[117,2191,239],{"class":134},[117,2193,2194,2197,2200],{"class":119,"line":162},[117,2195,2196],{"class":134},"  maxNodes: ",[117,2198,2199],{"class":257},"800",[117,2201,239],{"class":134},[117,2203,2204,2207,2210],{"class":119,"line":175},[117,2205,2206],{"class":134},"  minSize: ",[117,2208,2209],{"class":257},"4",[117,2211,239],{"class":134},[117,2213,2214],{"class":119,"line":181},[117,2215,438],{"class":134},[10,2217,2218,206,2221,2224,2225,2228,2229,2232,2233,2236,2237,2240,2241,2243],{},[29,2219,2220],{},"maxDepth",[29,2222,2223],{},"maxNodes",", and ",[29,2226,2227],{},"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 ",[29,2230,2231],{},"truncated: true",", and the wrapper logs a one-time ",[29,2234,2235],{},"console.warn"," per ",[29,2238,2239],{},"cacheKey"," so you notice during development instead of in production. ",[29,2242,2227],{}," 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,2245,2246,2247,2249],{},"The trade-off clone mode pays is that it's client-side only. You need a real DOM, ",[29,2248,2056],{},", 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.",[22,2251,2253],{"id":2252},"mirror-mode-walk-the-vnodes-ship-to-the-server","Mirror mode: walk the vnodes, ship to the server",[10,2255,2256,2257,2260],{},"Mirror swaps the engine. Instead of mounting the slot and reading the DOM, it walks the vnode tree at render time. No ",[29,2258,2259],{},"window"," access. Hydration-clean. Server-renderable.",[108,2262,2264],{"className":1896,"code":2263,"language":1898,"meta":113,"style":113},"\u003CASkeleton mode=\"mirror\" :loading=\"loading\">\n  \u003CSomeRichComponent \u002F>\n\u003C\u002FASkeleton>\n",[29,2265,2266,2294,2298],{"__ignoreMap":113},[117,2267,2268,2270,2272,2275,2277,2280,2282,2284,2286,2288,2290,2292],{"class":119,"line":120},[117,2269,135],{"class":134},[117,2271,2096],{"class":1907},[117,2273,2274],{"class":130}," mode",[117,2276,683],{"class":134},[117,2278,2279],{"class":168},"\"mirror\"",[117,2281,2099],{"class":134},[117,2283,2102],{"class":130},[117,2285,683],{"class":134},[117,2287,1916],{"class":168},[117,2289,2102],{"class":134},[117,2291,1916],{"class":168},[117,2293,1924],{"class":134},[117,2295,2296],{"class":119,"line":144},[117,2297,2117],{"class":134},[117,2299,2300,2302,2304],{"class":119,"line":150},[117,2301,1939],{"class":134},[117,2303,2096],{"class":1907},[117,2305,1924],{"class":134},[10,2307,2308,2309,2312,2313,2316,2317,206,2320,206,2323,2326,2327,2330],{},"The walker preserves every element's tag and class. Text-bearing leaves become ",[29,2310,2311],{},"\u003Cspan class=\"a-skel-text-content\">"," (transparent text on a skeleton background) with ",[29,2314,2315],{},"box-decoration-break: clone"," so the shimmer wraps the exact rendered text width per line. Atomic and interactive tags (",[29,2318,2319],{},"\u003Cimg>",[29,2321,2322],{},"\u003Cbutton>",[29,2324,2325],{},"\u003Csvg>",") become ",[29,2328,2329],{},"\u003Cdiv class=\"a-skel-block\">"," sized from their class-derived dimensions.",[10,2332,2333,2334,2336],{},"Mirror's win is SSR. Its trade-off is fidelity. Without ",[29,2335,2056],{},", 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,2338,2339],{},"Both modes share the same animation pipeline, the same cache, the same a11y baseline. Switching between them is a single prop.",[22,2341,2343],{"id":2342},"structural-mode-trees-in-normal-flow-persisted-between-sessions","Structural mode: trees in normal flow, persisted between sessions",[10,2345,2346,2347,2350,2351,17],{},"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 ",[29,2348,2349],{},"useSkeleton()"," + ",[29,2352,2353],{},"\u003CASkeletonLayer>",[108,2355,2357],{"className":1896,"code":2356,"language":1898,"meta":113,"style":113},"\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",[29,2358,2359,2379,2393,2406,2410,2435,2453,2479,2506,2510,2534,2553,2574,2584,2588,2592,2620,2628,2632,2641,2657,2683,2700,2720,2729],{"__ignoreMap":113},[117,2360,2361,2363,2366,2369,2372,2374,2377],{"class":119,"line":120},[117,2362,135],{"class":134},[117,2364,2365],{"class":1907},"script",[117,2367,2368],{"class":130}," setup",[117,2370,2371],{"class":130}," lang",[117,2373,683],{"class":134},[117,2375,2376],{"class":168},"\"ts\"",[117,2378,1924],{"class":134},[117,2380,2381,2383,2386,2388,2391],{"class":119,"line":144},[117,2382,2140],{"class":123},[117,2384,2385],{"class":134}," { computed, ref } ",[117,2387,2152],{"class":123},[117,2389,2390],{"class":168}," 'vue'",[117,2392,527],{"class":134},[117,2394,2395,2397,2400,2402,2404],{"class":119,"line":150},[117,2396,2140],{"class":123},[117,2398,2399],{"class":134}," { useSkeleton } ",[117,2401,2152],{"class":123},[117,2403,2155],{"class":168},[117,2405,527],{"class":134},[117,2407,2408],{"class":119,"line":156},[117,2409,295],{"emptyLinePlaceholder":294},[117,2411,2412,2414,2417,2419,2422,2425,2428,2430,2432],{"class":119,"line":162},[117,2413,616],{"class":123},[117,2415,2416],{"class":257}," props",[117,2418,350],{"class":123},[117,2420,2421],{"class":130}," defineProps",[117,2423,2424],{"class":134},"\u003C{ ",[117,2426,2427],{"class":202},"userId",[117,2429,512],{"class":123},[117,2431,524],{"class":257},[117,2433,2434],{"class":134}," }>();\n",[117,2436,2437,2439,2442,2444,2447,2449,2451],{"class":119,"line":175},[117,2438,616],{"class":123},[117,2440,2441],{"class":257}," user",[117,2443,350],{"class":123},[117,2445,2446],{"class":130}," ref",[117,2448,199],{"class":134},[117,2450,2013],{"class":257},[117,2452,976],{"class":134},[117,2454,2455,2457,2460,2462,2465,2468,2470,2473,2475,2477],{"class":119,"line":181},[117,2456,616],{"class":123},[117,2458,2459],{"class":257}," loading",[117,2461,350],{"class":123},[117,2463,2464],{"class":130}," computed",[117,2466,2467],{"class":134},"(() ",[117,2469,251],{"class":123},[117,2471,2472],{"class":134}," user.value ",[117,2474,1068],{"class":123},[117,2476,1082],{"class":257},[117,2478,976],{"class":134},[117,2480,2481,2483,2486,2488,2490,2492,2495,2497,2499,2502,2504],{"class":119,"line":193},[117,2482,616],{"class":123},[117,2484,2485],{"class":257}," containerRef",[117,2487,350],{"class":123},[117,2489,2446],{"class":130},[117,2491,135],{"class":134},[117,2493,2494],{"class":130},"HTMLElement",[117,2496,1556],{"class":123},[117,2498,1082],{"class":257},[117,2500,2501],{"class":134},">(",[117,2503,2013],{"class":257},[117,2505,976],{"class":134},[117,2507,2508],{"class":119,"line":215},[117,2509,295],{"emptyLinePlaceholder":294},[117,2511,2512,2514,2516,2519,2521,2524,2527,2529,2532],{"class":119,"line":221},[117,2513,616],{"class":123},[117,2515,549],{"class":134},[117,2517,2518],{"class":257},"shape",[117,2520,206],{"class":134},[117,2522,2523],{"class":257},"clear",[117,2525,2526],{"class":134}," } ",[117,2528,683],{"class":123},[117,2530,2531],{"class":130}," useSkeleton",[117,2533,227],{"class":134},[117,2535,2536,2539,2542,2545,2547,2549,2551],{"class":119,"line":230},[117,2537,2538],{"class":134},"  cacheKey: ",[117,2540,2541],{"class":168},"`user-card:${",[117,2543,2544],{"class":134},"props",[117,2546,17],{"class":168},[117,2548,2427],{"class":134},[117,2550,692],{"class":168},[117,2552,239],{"class":134},[117,2554,2555,2558,2560,2562,2565,2567,2569,2571],{"class":119,"line":242},[117,2556,2557],{"class":130},"  target",[117,2559,248],{"class":134},[117,2561,251],{"class":123},[117,2563,2564],{"class":134}," (loading.value ",[117,2566,1370],{"class":123},[117,2568,1082],{"class":257},[117,2570,2099],{"class":123},[117,2572,2573],{"class":134}," containerRef.value),\n",[117,2575,2576,2579,2582],{"class":119,"line":285},[117,2577,2578],{"class":134},"  persist: ",[117,2580,2581],{"class":257},"true",[117,2583,239],{"class":134},[117,2585,2586],{"class":119,"line":291},[117,2587,438],{"class":134},[117,2589,2590],{"class":119,"line":298},[117,2591,295],{"emptyLinePlaceholder":294},[117,2593,2594,2597,2600,2603,2605,2608,2610,2612,2615,2617],{"class":119,"line":304},[117,2595,2596],{"class":130},"fetchUser",[117,2598,2599],{"class":134},"(props.userId).",[117,2601,2602],{"class":130},"then",[117,2604,665],{"class":134},[117,2606,2607],{"class":202},"u",[117,2609,671],{"class":134},[117,2611,251],{"class":123},[117,2613,2614],{"class":134}," (user.value ",[117,2616,683],{"class":123},[117,2618,2619],{"class":134}," u));\n",[117,2621,2622,2624,2626],{"class":119,"line":330},[117,2623,1939],{"class":134},[117,2625,2365],{"class":1907},[117,2627,1924],{"class":134},[117,2629,2630],{"class":119,"line":335},[117,2631,295],{"emptyLinePlaceholder":294},[117,2633,2634,2636,2639],{"class":119,"line":341},[117,2635,135],{"class":134},[117,2637,2638],{"class":1907},"template",[117,2640,1924],{"class":134},[117,2642,2643,2646,2648,2650,2652,2655],{"class":119,"line":358},[117,2644,2645],{"class":134},"  \u003C",[117,2647,1908],{"class":1907},[117,2649,2446],{"class":130},[117,2651,683],{"class":134},[117,2653,2654],{"class":168},"\"containerRef\"",[117,2656,1924],{"class":134},[117,2658,2659,2662,2665,2667,2669,2672,2675,2677,2680],{"class":119,"line":368},[117,2660,2661],{"class":134},"    \u003C",[117,2663,2664],{"class":1907},"ASkeletonLayer",[117,2666,1911],{"class":130},[117,2668,683],{"class":134},[117,2670,2671],{"class":168},"\"loading && shape\"",[117,2673,2674],{"class":130}," :shape",[117,2676,683],{"class":134},[117,2678,2679],{"class":168},"\"shape\"",[117,2681,2682],{"class":134}," \u002F>\n",[117,2684,2685,2687,2690,2693,2695,2698],{"class":119,"line":380},[117,2686,2661],{"class":134},[117,2688,2689],{"class":1907},"ColdStartFallback",[117,2691,2692],{"class":130}," v-else-if",[117,2694,683],{"class":134},[117,2696,2697],{"class":168},"\"loading\"",[117,2699,2682],{"class":134},[117,2701,2702,2704,2707,2710,2713,2715,2718],{"class":119,"line":385},[117,2703,2661],{"class":134},[117,2705,2706],{"class":1907},"UserCard",[117,2708,2709],{"class":130}," v-else",[117,2711,2712],{"class":130}," :data",[117,2714,683],{"class":134},[117,2716,2717],{"class":168},"\"user\"",[117,2719,2682],{"class":134},[117,2721,2722,2725,2727],{"class":119,"line":413},[117,2723,2724],{"class":134},"  \u003C\u002F",[117,2726,1908],{"class":1907},[117,2728,1924],{"class":134},[117,2730,2731,2733,2735],{"class":119,"line":425},[117,2732,1939],{"class":134},[117,2734,2638],{"class":1907},[117,2736,1924],{"class":134},[10,2738,2739,2740,2743,2744,206,2747,206,2750,206,2753,206,2756,206,2759,2762,2763,2765,2766,2769,2770,2773],{},"The shape is captured by ",[29,2741,2742],{},"walkStructural()",", a tree walker that preserves container tags, their original classes, and the resolved layout CSS (",[29,2745,2746],{},"display",[29,2748,2749],{},"flex-*",[29,2751,2752],{},"gap",[29,2754,2755],{},"padding",[29,2757,2758],{},"grid-*",[29,2760,2761],{},"box-sizing","). Leaves carry inline width\u002Fheight plus the visual signals (background, border, radius). When ",[29,2764,2353],{}," replays the tree, it does so in ",[14,2767,2768],{},"normal flow",": the captured ",[29,2771,2772],{},"\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,2775,2776],{},"Given this real markup:",[108,2778,2782],{"className":2779,"code":2780,"language":2781,"meta":113,"style":113},"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",[29,2783,2784,2799,2812,2824,2837],{"__ignoreMap":113},[117,2785,2786,2788,2790,2792,2794,2797],{"class":119,"line":120},[117,2787,135],{"class":134},[117,2789,1908],{"class":1907},[117,2791,987],{"class":130},[117,2793,683],{"class":134},[117,2795,2796],{"class":168},"\"flex flex-col gap-4 p-4\"",[117,2798,1924],{"class":134},[117,2800,2801,2803,2805,2808,2810],{"class":119,"line":144},[117,2802,2645],{"class":134},[117,2804,465],{"class":1907},[117,2806,2807],{"class":134},">...\u003C\u002F",[117,2809,465],{"class":1907},[117,2811,1924],{"class":134},[117,2813,2814,2816,2818,2820,2822],{"class":119,"line":150},[117,2815,2645],{"class":134},[117,2817,10],{"class":1907},[117,2819,2807],{"class":134},[117,2821,10],{"class":1907},[117,2823,1924],{"class":134},[117,2825,2826,2828,2831,2833,2835],{"class":119,"line":156},[117,2827,2645],{"class":134},[117,2829,2830],{"class":1907},"button",[117,2832,2807],{"class":134},[117,2834,2830],{"class":1907},[117,2836,1924],{"class":134},[117,2838,2839,2841,2843],{"class":119,"line":162},[117,2840,1939],{"class":134},[117,2842,1908],{"class":1907},[117,2844,1924],{"class":134},[10,2846,2847],{},"The layer replays as:",[108,2849,2851],{"className":2779,"code":2850,"language":2781,"meta":113,"style":113},"\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",[29,2852,2853,2860,2870,2880,2884,2911,2934,2957],{"__ignoreMap":113},[117,2854,2855,2857],{"class":119,"line":120},[117,2856,135],{"class":134},[117,2858,2859],{"class":1907},"div\n",[117,2861,2862,2865,2867],{"class":119,"line":144},[117,2863,2864],{"class":130},"  class",[117,2866,683],{"class":134},[117,2868,2869],{"class":168},"\"flex flex-col gap-4 p-4\"\n",[117,2871,2872,2875,2877],{"class":119,"line":150},[117,2873,2874],{"class":130},"  style",[117,2876,683],{"class":134},[117,2878,2879],{"class":168},"\"display: flex; flex-direction: column; gap: 16px; padding: 16px; box-sizing: border-box\"\n",[117,2881,2882],{"class":119,"line":156},[117,2883,1924],{"class":134},[117,2885,2886,2888,2890,2892,2894,2897,2900,2902,2905,2909],{"class":119,"line":162},[117,2887,2645],{"class":134},[117,2889,1908],{"class":1907},[117,2891,987],{"class":130},[117,2893,683],{"class":134},[117,2895,2896],{"class":168},"\"a-skel\"",[117,2898,2899],{"class":130}," style",[117,2901,683],{"class":134},[117,2903,2904],{"class":168},"\"width: 200px; height: 24px; ...\"",[117,2906,2908],{"class":2907},"s7hpK"," \u002F",[117,2910,1924],{"class":134},[117,2912,2913,2915,2917,2919,2921,2923,2925,2927,2930,2932],{"class":119,"line":175},[117,2914,2645],{"class":134},[117,2916,1908],{"class":1907},[117,2918,987],{"class":130},[117,2920,683],{"class":134},[117,2922,2896],{"class":168},[117,2924,2899],{"class":130},[117,2926,683],{"class":134},[117,2928,2929],{"class":168},"\"width: 280px; height: 16px; ...\"",[117,2931,2908],{"class":2907},[117,2933,1924],{"class":134},[117,2935,2936,2938,2940,2942,2944,2946,2948,2950,2953,2955],{"class":119,"line":181},[117,2937,2645],{"class":134},[117,2939,1908],{"class":1907},[117,2941,987],{"class":130},[117,2943,683],{"class":134},[117,2945,2896],{"class":168},[117,2947,2899],{"class":130},[117,2949,683],{"class":134},[117,2951,2952],{"class":168},"\"width: 120px; height: 36px; ...\"",[117,2954,2908],{"class":2907},[117,2956,1924],{"class":134},[117,2958,2959,2961,2963],{"class":119,"line":193},[117,2960,1939],{"class":134},[117,2962,1908],{"class":1907},[117,2964,1924],{"class":134},[10,2966,2967],{},"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,2969,2970,2971,2974,2975,2978,2979,2982,2983,2986,2987,2990],{},"The other thing structural mode gives you is persistence. ",[29,2972,2973],{},"persist: true"," mirrors the captured shape into ",[29,2976,2977],{},"localStorage"," under the ",[29,2980,2981],{},"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 (",[29,2984,2985],{},"v: 3"," for structural, ",[29,2988,2989],{},"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,2992,2993],{},"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.",[22,2995,2997],{"id":2996},"the-capture-cache-and-replay-pipeline","The capture, cache, and replay pipeline",[10,2999,3000],{},"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,3002,3003,3006],{},[14,3004,3005],{},"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,3008,3009,3012,3013,3016,3017,3020,3021,3024,3025,3027,3028,3030,3031,3034,3035,3037,3038,3040,3041,3043],{},[14,3010,3011],{},"Data arrives."," The slot's real content renders. A ",[29,3014,3015],{},"ResizeObserver"," plus a ",[29,3018,3019],{},"requestAnimationFrame"," schedules a measurement pass. In clone mode, ",[29,3022,3023],{},"captureSnapshot()"," reads ",[29,3026,2056],{}," on every element and per-line text rects via ",[29,3029,2080],{},", then freezes the result into a ",[29,3032,3033],{},"CaptureSnapshot"," tree. In structural mode, ",[29,3036,2742],{}," 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 ",[29,3039,2977],{}," if ",[29,3042,2973],{},").",[10,3045,3046,3049,3050,3052,3053,3055,3056,3059,3060,3062],{},[14,3047,3048],{},"Next loading flip."," The user navigates back to the page, or triggers a refetch, or revisits the route on a return session. ",[29,3051,2102],{}," flips to ",[29,3054,2581],{},". The cached shape replays instantly, with no intermediate \"guess the shape\" step. In clone mode, ",[29,3057,3058],{},"\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, ",[29,3061,2353],{}," renders the tree in normal flow, with the captured layout CSS driving the flex\u002Fgrid container behaviour.",[10,3064,3065,3066,3068,3069,3071,3072,3074,3075,2236,3077,3079],{},"The budgets matter at every step. ",[29,3067,2223],{}," (default 500–800 depending on the walker) is the cap on tree size. ",[29,3070,2220],{}," (default 12–16) is the recursion cap. ",[29,3073,2227],{}," (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 ",[29,3076,2235],{},[29,3078,2239],{}," so you find out in dev rather than in production.",[10,3081,3082,3083,3086,3087,3089],{},"One layout read up front, then cached values for the rest of the walk. The ",[29,3084,3085],{},"getBoundingClientRect()"," and ",[29,3088,2056],{}," calls happen in a single top-down pass with no intervening writes, no layout thrash: one forced reflow, then the captured snapshot is durable.",[22,3091,3093],{"id":3092},"the-15-named-primitives","The 15 named primitives",[10,3095,3096,3097,206,3100,206,3103,206,3106,206,3109,206,3112,206,3115,206,3118,206,3121,206,3124,206,3127,206,3130,206,3133,206,3136,206,3139,17],{},"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: ",[29,3098,3099],{},"\u003CASkeletonCard>",[29,3101,3102],{},"\u003CASkeletonText>",[29,3104,3105],{},"\u003CASkeletonHeading>",[29,3107,3108],{},"\u003CASkeletonAvatar>",[29,3110,3111],{},"\u003CASkeletonImage>",[29,3113,3114],{},"\u003CASkeletonVideo>",[29,3116,3117],{},"\u003CASkeletonButton>",[29,3119,3120],{},"\u003CASkeletonInput>",[29,3122,3123],{},"\u003CASkeletonChip>",[29,3125,3126],{},"\u003CASkeletonListItem>",[29,3128,3129],{},"\u003CASkeletonTable>",[29,3131,3132],{},"\u003CASkeletonChart>",[29,3134,3135],{},"\u003CASkeletonForm>",[29,3137,3138],{},"\u003CASkeletonArticle>",[29,3140,3141],{},"\u003CASkeletonDivider>",[10,3143,3144],{},"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,3146,3147,3148,3151],{},"There's also ",[29,3149,3150],{},"\u003CASkeletonBlock>"," for the case where none of the variants fit and you want to compose your own:",[108,3153,3155],{"className":1896,"code":3154,"language":1898,"meta":113,"style":113},"\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",[29,3156,3157,3182,3187,3192,3197,3202,3207,3215,3223],{"__ignoreMap":113},[117,3158,3159,3161,3163,3165,3167,3169,3171,3173,3175,3177,3180],{"class":119,"line":120},[117,3160,135],{"class":134},[117,3162,1908],{"class":1907},[117,3164,1911],{"class":123},[117,3166,683],{"class":134},[117,3168,1916],{"class":168},[117,3170,2102],{"class":134},[117,3172,1916],{"class":168},[117,3174,987],{"class":130},[117,3176,683],{"class":134},[117,3178,3179],{"class":168},"\"flex items-start gap-4 p-4\"",[117,3181,1924],{"class":134},[117,3183,3184],{"class":119,"line":144},[117,3185,3186],{"class":134},"  \u003CASkeletonBlock type=\"circle\" :w=\"64\" :h=\"64\" \u002F>\n",[117,3188,3189],{"class":119,"line":150},[117,3190,3191],{"class":134},"  \u003Cdiv class=\"flex-1 space-y-2\">\n",[117,3193,3194],{"class":119,"line":156},[117,3195,3196],{"class":134},"    \u003CASkeletonBlock type=\"text\" :w=\"160\" :h=\"18\" \u002F>\n",[117,3198,3199],{"class":119,"line":162},[117,3200,3201],{"class":134},"    \u003CASkeletonBlock type=\"text\" :w=\"100\" :h=\"12\" \u002F>\n",[117,3203,3204],{"class":119,"line":175},[117,3205,3206],{"class":134},"    \u003CASkeletonBlock type=\"text\" :lines=\"3\" :h=\"14\" class=\"!mt-3\" \u002F>\n",[117,3208,3209,3211,3213],{"class":119,"line":181},[117,3210,2724],{"class":134},[117,3212,1908],{"class":1907},[117,3214,1924],{"class":134},[117,3216,3217,3219,3221],{"class":119,"line":193},[117,3218,1939],{"class":134},[117,3220,1908],{"class":1907},[117,3222,1924],{"class":134},[117,3224,3225],{"class":119,"line":215},[117,3226,3227],{"class":134},"\u003CUserCard v-else :data=\"user\" \u002F>\n",[10,3229,3230,3232,3233,3236,3237,3240],{},[29,3231,3150],{}," is flow-friendly. It composes with flex, grid, stack layouts. The ",[29,3234,3235],{},"lines"," prop on ",[29,3238,3239],{},"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.",[22,3242,3244],{"id":3243},"animations-and-theming-as-strategies","Animations and theming, as strategies",[10,3246,3247,3248,3251,3252,3255,3256,3259,3260,3263,3264,3267,3268,3271,3272,3275,3276,3279,3280,3283,3284,3287],{},"Four animation modes ship: ",[29,3249,3250],{},"shimmer"," (a gradient sweep on a ",[29,3253,3254],{},"::after"," pseudo-element, contained per block with ",[29,3257,3258],{},"overflow: hidden","), ",[29,3261,3262],{},"pulse"," (opacity cycling from 1 to ",[29,3265,3266],{},"--ak-skel-pulse-min"," and back), ",[29,3269,3270],{},"wave"," (a sliding gradient via ",[29,3273,3274],{},"background-position","), and ",[29,3277,3278],{},"none"," (static blocks). Each is a strategy, not a branch. You pass ",[29,3281,3282],{},"animation=\"pulse\""," and the right CSS class engages. No ",[29,3285,3286],{},"if (animation === 'pulse')"," chain in the component.",[10,3289,3290,3293],{},[29,3291,3292],{},"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,3295,3296,3297,3300,3301,3304],{},"Theming is CSS variables. The tokens are scoped under ",[29,3298,3299],{},"--ak-skel-*"," (with backward-compat aliases under ",[29,3302,3303],{},"--ak-skeleton-*"," for v1 consumers):",[3306,3307,3308,3321],"table",{},[3309,3310,3311],"thead",{},[3312,3313,3314,3318],"tr",{},[3315,3316,3317],"th",{},"Token",[3315,3319,3320],{},"Used for",[3322,3323,3324,3335,3345,3355,3365,3374,3384],"tbody",{},[3312,3325,3326,3332],{},[3327,3328,3329],"td",{},[29,3330,3331],{},"--ak-skel-base",[3327,3333,3334],{},"Block fill",[3312,3336,3337,3342],{},[3327,3338,3339],{},[29,3340,3341],{},"--ak-skel-highlight",[3327,3343,3344],{},"Shimmer \u002F wave sweep colour",[3312,3346,3347,3352],{},[3327,3348,3349],{},[29,3350,3351],{},"--ak-skel-radius",[3327,3353,3354],{},"Default block border radius",[3312,3356,3357,3362],{},[3327,3358,3359],{},[29,3360,3361],{},"--ak-skel-duration",[3327,3363,3364],{},"Animation cycle length",[3312,3366,3367,3371],{},[3327,3368,3369],{},[29,3370,3266],{},[3327,3372,3373],{},"Opacity at the trough of the pulse cycle",[3312,3375,3376,3381],{},[3327,3377,3378],{},[29,3379,3380],{},"--ak-skel-ring",[3327,3382,3383],{},"Subtle 1-px inset ring colour",[3312,3385,3386,3391],{},[3327,3387,3388],{},[29,3389,3390],{},"--ak-skel-icon",[3327,3392,3393],{},"Placeholder icon colour (image \u002F video variants)",[10,3395,3396,3397,3400],{},"Override them on ",[29,3398,3399],{},":root",", on a wrapper class for multi-tenant themes, or inline on a single instance:",[108,3402,3406],{"className":3403,"code":3404,"language":3405,"meta":113,"style":113},"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",[29,3407,3408,3415,3443,3473,3488,3503],{"__ignoreMap":113},[117,3409,3410,3413],{"class":119,"line":120},[117,3411,3412],{"class":130},".tenant-acme",[117,3414,410],{"class":134},[117,3416,3417,3420,3422,3425,3427,3430,3433,3436,3439,3441],{"class":119,"line":144},[117,3418,3419],{"class":202},"  --ak-skel-base",[117,3421,851],{"class":134},[117,3423,3424],{"class":257},"hsl",[117,3426,199],{"class":134},[117,3428,3429],{"class":257},"220",[117,3431,3432],{"class":257}," 30",[117,3434,3435],{"class":123},"%",[117,3437,3438],{"class":257}," 18",[117,3440,3435],{"class":123},[117,3442,976],{"class":134},[117,3444,3445,3448,3450,3452,3454,3456,3459,3461,3463,3465,3468,3471],{"class":119,"line":150},[117,3446,3447],{"class":202},"  --ak-skel-highlight",[117,3449,851],{"class":134},[117,3451,3424],{"class":257},[117,3453,199],{"class":134},[117,3455,3429],{"class":257},[117,3457,3458],{"class":257}," 60",[117,3460,3435],{"class":123},[117,3462,3458],{"class":257},[117,3464,3435],{"class":123},[117,3466,3467],{"class":134}," \u002F ",[117,3469,3470],{"class":257},"0.35",[117,3472,976],{"class":134},[117,3474,3475,3478,3480,3483,3486],{"class":119,"line":156},[117,3476,3477],{"class":202},"  --ak-skel-radius",[117,3479,851],{"class":134},[117,3481,3482],{"class":257},"0.5",[117,3484,3485],{"class":123},"rem",[117,3487,527],{"class":134},[117,3489,3490,3493,3495,3498,3501],{"class":119,"line":162},[117,3491,3492],{"class":202},"  --ak-skel-duration",[117,3494,851],{"class":134},[117,3496,3497],{"class":257},"2",[117,3499,3500],{"class":123},"s",[117,3502,527],{"class":134},[117,3504,3505],{"class":119,"line":175},[117,3506,589],{"class":134},[10,3508,3509,3510,3513,3514,3517,3518,3521,3522,3524,3525,3528],{},"Dark mode is automatic via ",[29,3511,3512],{},":where(.dark)"," selectors that retint the tokens (Tailwind's ",[29,3515,3516],{},".dark"," class, shadcn's, ",[29,3519,3520],{},"nuxt-color-mode","'s, anything that lands a ",[29,3523,3516],{}," ancestor), plus a ",[29,3526,3527],{},"@media (prefers-color-scheme: dark)"," fallback for the case where no explicit class is on the tree.",[10,3530,3531],{},"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.",[22,3533,3535],{"id":3534},"a11y-you-dont-have-to-configure","A11y you don't have to configure",[10,3537,3538,3539,3542,3543,3546,3547,3550,3551,3554],{},"The defaults are the ones you'd want anyway. Every wrapper and variant root carries ",[29,3540,3541],{},"role=\"status\""," while loading, ",[29,3544,3545],{},"aria-busy=\"true\""," mirroring the loading state, and ",[29,3548,3549],{},"aria-live=\"polite\""," so screen readers announce the loading state without interrupting the user. A visually-hidden ",[29,3552,3553],{},"\u003Cspan class=\"a-skel-sr-only\">Loading...\u003C\u002Fspan>"," ships with every variant so the announcement has text to read.",[10,3556,3557,3558,3561,3562,3086,3565,3568,3569,3572],{},"Every emitted shimmer surface carries ",[29,3559,3560],{},"aria-hidden=\"true\"",": the placeholder is decorative; the wrapper is what gets announced. Mirror-mode skeletons disable ",[29,3563,3564],{},"user-select",[29,3566,3567],{},"pointer-events"," on the slot tree so the placeholder can't be interacted with mid-load. ",[29,3570,3571],{},"prefers-reduced-motion"," strips animation.",[10,3574,3575,3576,3579],{},"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 ",[29,3577,3578],{},"animation=\"none\""," is the lever) or doing something a11y-wise that I don't want to make easy.",[22,3581,3583],{"id":3582},"what-clone-mode-taught-me","What clone mode taught me",[10,3585,3586,3587,3590,3591,3594],{},"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 ",[29,3588,3589],{},"border-radius: 0.75rem"," mixed with ",[29,3592,3593],{},"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,3596,3597,3598,3600],{},"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 ",[29,3599,2056],{}," will tell you if you ask.",[10,3602,3603],{},"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,3605,3606],{},"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.",[1812,3608,3609],{},"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":113,"searchDepth":144,"depth":144,"links":3611},[3612,3613,3614,3615,3616,3617,3618,3619,3620],{"id":1884,"depth":144,"text":1885},{"id":2034,"depth":144,"text":2035},{"id":2252,"depth":144,"text":2253},{"id":2342,"depth":144,"text":2343},{"id":2996,"depth":144,"text":2997},{"id":3092,"depth":144,"text":3093},{"id":3243,"depth":144,"text":3244},{"id":3534,"depth":144,"text":3535},{"id":3582,"depth":144,"text":3583},"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، ومسار العمل الذي يلتقط ثم يُخزّن مؤقّتاً ثم يُعيد العرض خلفها.",[3625,3626,3627,3628,3629,3630,3015,1847,3631,3632],"Vue 3","Nuxt","skeleton loader","loading states","getComputedStyle","Range API","component library","accessibility",{},"\u002Fwrites\u002Finside-a-skeleton-self-generating-skeleton-loaders",{"title":1867,"description":3622},"writes\u002Finside-a-skeleton-self-generating-skeleton-loaders","داخل @alikhalilll\u002Fa-skeleton، مُحمِّلات skeleton تولّد نفسها بنفسها","VxGF-EgIL4tFkWPV2BLDCQVNC3i8p1quakbSMMxOdpw",{"id":3640,"title":3641,"body":3642,"date":8140,"description":8141,"description_ar":8142,"draft":1841,"extension":1842,"image":1843,"keywords":8143,"lang":1857,"meta":8151,"navigation":294,"path":8152,"seo":8153,"stem":8154,"title_ar":8155,"updatedAt":8140,"__hash__":8156},"writes\u002Fwrites\u002Fatell-input-the-library-rewrite.md","Notes from rewriting ATellInput as a published library",{"type":7,"value":3643,"toc":8130},[3644,3650,3653,3660,3663,3667,3670,3730,3744,3747,3763,3766,3853,3879,4023,4040,4051,4054,4058,4061,4149,4159,4178,4184,4737,4740,4758,4786,4801,4804,4807,4982,4999,5071,5074,5078,5085,5091,5419,5422,5489,5492,5814,5841,5847,5890,5901,5904,5986,5995,5999,6002,6009,6120,6138,6141,6145,6148,6151,6716,6758,6765,6905,6918,6921,7051,7058,7061,7195,7209,7212,7216,7243,7246,7478,7485,7709,7712,7886,7893,7896,7900,7906,7911,8032,8042,8045,8049,8060,8067,8070,8113,8116,8127],[10,3645,3646,3649],{},[29,3647,3648],{},"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,3651,3652],{},"This post is what happened when I tried to publish it.",[10,3654,3655,3656,3659],{},"The component now lives in ",[29,3657,3658],{},"@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,3661,3662],{},"I've grouped the changes around the seven that earned their place.",[22,3664,3666],{"id":3665},"two-models-two-shapes-number-out-iso2-inside","Two models, two shapes: number out, ISO2 inside",[10,3668,3669],{},"The old surface looked clean from a distance:",[108,3671,3673],{"className":110,"code":3672,"language":112,"meta":113,"style":113},"const phoneModelValue = defineModel\u003Cstring>('phone', { required: true });\nconst countryModelValue = defineModel\u003Cstring>('country', { required: true });\n",[29,3674,3675,3704],{"__ignoreMap":113},[117,3676,3677,3679,3682,3684,3687,3689,3692,3694,3697,3700,3702],{"class":119,"line":120},[117,3678,616],{"class":123},[117,3680,3681],{"class":257}," phoneModelValue",[117,3683,350],{"class":123},[117,3685,3686],{"class":130}," defineModel",[117,3688,135],{"class":134},[117,3690,3691],{"class":257},"string",[117,3693,2501],{"class":134},[117,3695,3696],{"class":168},"'phone'",[117,3698,3699],{"class":134},", { required: ",[117,3701,2581],{"class":257},[117,3703,327],{"class":134},[117,3705,3706,3708,3711,3713,3715,3717,3719,3721,3724,3726,3728],{"class":119,"line":144},[117,3707,616],{"class":123},[117,3709,3710],{"class":257}," countryModelValue",[117,3712,350],{"class":123},[117,3714,3686],{"class":130},[117,3716,135],{"class":134},[117,3718,3691],{"class":257},[117,3720,2501],{"class":134},[117,3722,3723],{"class":168},"'country'",[117,3725,3699],{"class":134},[117,3727,2581],{"class":257},[117,3729,327],{"class":134},[10,3731,3732,3735,3736,3739,3740,3743],{},[29,3733,3734],{},"country"," was a string of dial digits: ",[29,3737,3738],{},"\"20\""," for Egypt, ",[29,3741,3742],{},"\"44\""," for the UK. Easy to serialize, easy to URL-encode, easy to put in a database column.",[10,3745,3746],{},"It also couldn't tell US from Canada.",[10,3748,3749,3752,3753,3756,3757,3759,3760,3762],{},[29,3750,3751],{},"+1"," is shared by 25+ NANP countries. If a Canadian user picked Canada in the dropdown and the v-model emitted ",[29,3754,3755],{},"\"1\"",", the parent had no way to know which ",[29,3758,3751],{}," was meant. Reload the page, hydrate from the URL, and you'd get back ",[29,3761,3755],{}," 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,3764,3765],{},"The new model splits it:",[108,3767,3769],{"className":110,"code":3768,"language":112,"meta":113,"style":113},"const phone = defineModel\u003Cstring>('phone', { default: '' });\nconst country = defineModel\u003Cnumber | null>('country', { default: null });\n\nconst selectedIso2 = ref\u003Cstring>('');\n",[29,3770,3771,3798,3828,3832],{"__ignoreMap":113},[117,3772,3773,3775,3778,3780,3782,3784,3786,3788,3790,3793,3796],{"class":119,"line":120},[117,3774,616],{"class":123},[117,3776,3777],{"class":257}," phone",[117,3779,350],{"class":123},[117,3781,3686],{"class":130},[117,3783,135],{"class":134},[117,3785,3691],{"class":257},[117,3787,2501],{"class":134},[117,3789,3696],{"class":168},[117,3791,3792],{"class":134},", { default: ",[117,3794,3795],{"class":168},"''",[117,3797,327],{"class":134},[117,3799,3800,3802,3805,3807,3809,3811,3814,3816,3818,3820,3822,3824,3826],{"class":119,"line":144},[117,3801,616],{"class":123},[117,3803,3804],{"class":257}," country",[117,3806,350],{"class":123},[117,3808,3686],{"class":130},[117,3810,135],{"class":134},[117,3812,3813],{"class":257},"number",[117,3815,1556],{"class":123},[117,3817,1082],{"class":257},[117,3819,2501],{"class":134},[117,3821,3723],{"class":168},[117,3823,3792],{"class":134},[117,3825,2013],{"class":257},[117,3827,327],{"class":134},[117,3829,3830],{"class":119,"line":150},[117,3831,295],{"emptyLinePlaceholder":294},[117,3833,3834,3836,3839,3841,3843,3845,3847,3849,3851],{"class":119,"line":156},[117,3835,616],{"class":123},[117,3837,3838],{"class":257}," selectedIso2",[117,3840,350],{"class":123},[117,3842,2446],{"class":130},[117,3844,135],{"class":134},[117,3846,3691],{"class":257},[117,3848,2501],{"class":134},[117,3850,3795],{"class":168},[117,3852,976],{"class":134},[10,3854,3855,3857,3858,206,3861,206,3864,3866,3867,3870,3871,3874,3875,3878],{},[29,3856,3734],{}," is a number now (",[29,3859,3860],{},"20",[29,3862,3863],{},"1",[29,3865,2013],{},") because dial codes ",[443,3868,3869],{},"are"," numbers, and ",[29,3872,3873],{},"Number"," survives URL\u002FDB\u002FJSON round-trips with no string-trimming weirdness. But the component also tracks an internal ",[29,3876,3877],{},"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:",[108,3880,3882],{"className":110,"code":3881,"language":112,"meta":113,"style":113},"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",[29,3883,3884,3891,3896,3910,3925,3940,3947,3952,3973,3993,4005,4009,4019],{"__ignoreMap":113},[117,3885,3886,3889],{"class":119,"line":120},[117,3887,3888],{"class":130},"watch",[117,3890,966],{"class":134},[117,3892,3893],{"class":119,"line":144},[117,3894,3895],{"class":134},"  country,\n",[117,3897,3898,3901,3904,3906,3908],{"class":119,"line":150},[117,3899,3900],{"class":134},"  (",[117,3902,3903],{"class":202},"next",[117,3905,671],{"class":134},[117,3907,251],{"class":123},[117,3909,410],{"class":134},[117,3911,3912,3915,3918,3921,3923],{"class":119,"line":156},[117,3913,3914],{"class":123},"    if",[117,3916,3917],{"class":134}," (next ",[117,3919,3920],{"class":123},"==",[117,3922,1082],{"class":257},[117,3924,212],{"class":134},[117,3926,3927,3930,3933,3935,3938],{"class":119,"line":162},[117,3928,3929],{"class":123},"      if",[117,3931,3932],{"class":134}," (selectedIso2.value) selectedIso2.value ",[117,3934,683],{"class":123},[117,3936,3937],{"class":168}," ''",[117,3939,527],{"class":134},[117,3941,3942,3945],{"class":119,"line":175},[117,3943,3944],{"class":123},"      return",[117,3946,527],{"class":134},[117,3948,3949],{"class":119,"line":181},[117,3950,3951],{"class":134},"    }\n",[117,3953,3954,3956,3958,3961,3964,3966,3969,3971],{"class":119,"line":193},[117,3955,3914],{"class":123},[117,3957,1359],{"class":134},[117,3959,3960],{"class":130},"dialNumberFor",[117,3962,3963],{"class":134},"(selectedIso2.value) ",[117,3965,1068],{"class":123},[117,3967,3968],{"class":134}," next) ",[117,3970,1423],{"class":123},[117,3972,527],{"class":134},[117,3974,3975,3977,3980,3982,3985,3987,3990],{"class":119,"line":215},[117,3976,344],{"class":123},[117,3978,3979],{"class":257}," iso2",[117,3981,350],{"class":123},[117,3983,3984],{"class":130}," resolveCountryIdentifier",[117,3986,199],{"class":134},[117,3988,3989],{"class":130},"String",[117,3991,3992],{"class":134},"(next));\n",[117,3994,3995,3997,4000,4002],{"class":119,"line":221},[117,3996,3914],{"class":123},[117,3998,3999],{"class":134}," (iso2) selectedIso2.value ",[117,4001,683],{"class":123},[117,4003,4004],{"class":134}," iso2;\n",[117,4006,4007],{"class":119,"line":230},[117,4008,178],{"class":134},[117,4010,4011,4014,4016],{"class":119,"line":242},[117,4012,4013],{"class":134},"  { immediate: ",[117,4015,2581],{"class":257},[117,4017,4018],{"class":134}," }\n",[117,4020,4021],{"class":119,"line":285},[117,4022,976],{"class":134},[10,4024,4025,4026,4029,4030,4033,4034,4036,4037,4039],{},"The ",[29,4027,4028],{},"if (dialNumberFor(selectedIso2.value) === next) return;"," line is the one I want to call out. When the parent writes ",[29,4031,4032],{},"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 ",[29,4035,3877],{},".\nWithout that guard, the watcher would resolve ",[29,4038,3755],{}," to its alphabetically-first NANP country and silently flip Canada to whatever that is. The guard preserves the richer state.",[10,4041,4042,4043,4046,4047,4050],{},"The outward watch is ",[29,4044,4045],{},"flush: 'sync'"," so a separate ",[29,4048,4049],{},"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,4052,4053],{},"Two halves of the country, two distinct models, one source of truth for each.",[22,4055,4057],{"id":4056},"detection-first-the-picker-is-hidden-by-default","Detection-first: the picker is hidden by default",[10,4059,4060],{},"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:",[108,4062,4064],{"className":1896,"code":4063,"language":1898,"meta":113,"style":113},"\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",[29,4065,4066,4073,4083,4093,4103,4112,4122,4131,4135,4140],{"__ignoreMap":113},[117,4067,4068,4070],{"class":119,"line":120},[117,4069,135],{"class":134},[117,4071,4072],{"class":1907},"Transition\n",[117,4074,4075,4078,4080],{"class":119,"line":144},[117,4076,4077],{"class":130},"  enter-active-class",[117,4079,683],{"class":134},[117,4081,4082],{"class":168},"\"transition-all duration-200 ease-out overflow-hidden\"\n",[117,4084,4085,4088,4090],{"class":119,"line":150},[117,4086,4087],{"class":130},"  leave-active-class",[117,4089,683],{"class":134},[117,4091,4092],{"class":168},"\"transition-all duration-150 ease-in overflow-hidden\"\n",[117,4094,4095,4098,4100],{"class":119,"line":156},[117,4096,4097],{"class":130},"  enter-from-class",[117,4099,683],{"class":134},[117,4101,4102],{"class":168},"\"opacity-0 -translate-x-1 max-w-0\"\n",[117,4104,4105,4108,4110],{"class":119,"line":162},[117,4106,4107],{"class":130},"  leave-to-class",[117,4109,683],{"class":134},[117,4111,4102],{"class":168},[117,4113,4114,4117,4119],{"class":119,"line":175},[117,4115,4116],{"class":130},"  enter-to-class",[117,4118,683],{"class":134},[117,4120,4121],{"class":168},"\"max-w-[12rem]\"\n",[117,4123,4124,4127,4129],{"class":119,"line":181},[117,4125,4126],{"class":130},"  leave-from-class",[117,4128,683],{"class":134},[117,4130,4121],{"class":168},[117,4132,4133],{"class":119,"line":193},[117,4134,1924],{"class":134},[117,4136,4137],{"class":119,"line":215},[117,4138,4139],{"class":134},"  \u003CACountrySelect v-if=\"!props.detectFromInput || selectedIso2\" ... \u002F>\n",[117,4141,4142,4144,4147],{"class":119,"line":221},[117,4143,1939],{"class":134},[117,4145,4146],{"class":1907},"Transition",[117,4148,1924],{"class":134},[10,4150,4151,4154,4155,4158],{},[29,4152,4153],{},"max-w"," collapses to zero when nothing is selected, animates open on the first successful match. ",[29,4156,4157],{},"detectFromInput=\"false\""," brings the always-visible picker back for callers who want the legacy shape.",[10,4160,4161,4162,4165,4166,4169,4170,4173,4174,4177],{},"The behaviour is opinionated. Most users in most countries ",[443,4163,4164],{},"never"," type a ",[29,4167,4168],{},"+"," prefix. They type the number they would type into their own phone: ",[29,4171,4172],{},"01066105963"," in Cairo, ",[29,4175,4176],{},"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,4179,4180,4181,512],{},"The \"detect what they're doing\" part is the only non-trivial piece. It runs through three tiers in ",[29,4182,4183],{},"matchLeadingDialCode",[108,4185,4187],{"className":110,"code":4186,"language":112,"meta":113,"style":113},"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",[29,4188,4189,4219,4236,4240,4245,4252,4275,4288,4304,4311,4332,4337,4341,4351,4356,4360,4364,4369,4381,4404,4411,4432,4445,4466,4474,4492,4497,4501,4511,4517,4522,4527,4532,4538,4587,4610,4626,4647,4668,4695,4701,4707,4718,4723,4732],{"__ignoreMap":113},[117,4190,4191,4194,4197,4199,4202,4204,4206,4208,4210,4213,4215,4217],{"class":119,"line":120},[117,4192,4193],{"class":123},"function",[117,4195,4196],{"class":130}," matchLeadingDialCode",[117,4198,199],{"class":134},[117,4200,4201],{"class":202},"digits",[117,4203,512],{"class":123},[117,4205,524],{"class":257},[117,4207,271],{"class":134},[117,4209,512],{"class":123},[117,4211,4212],{"class":130}," DialMatch",[117,4214,1556],{"class":123},[117,4216,1082],{"class":257},[117,4218,410],{"class":134},[117,4220,4221,4223,4225,4227,4230,4232,4234],{"class":119,"line":144},[117,4222,1417],{"class":123},[117,4224,1359],{"class":134},[117,4226,1589],{"class":123},[117,4228,4229],{"class":134},"digits) ",[117,4231,1423],{"class":123},[117,4233,1082],{"class":257},[117,4235,527],{"class":134},[117,4237,4238],{"class":119,"line":150},[117,4239,295],{"emptyLinePlaceholder":294},[117,4241,4242],{"class":119,"line":156},[117,4243,4244],{"class":187},"  \u002F\u002F Tier 1: international parse, disambiguates NANP and strips the calling code.\n",[117,4246,4247,4250],{"class":119,"line":162},[117,4248,4249],{"class":123},"  try",[117,4251,410],{"class":134},[117,4253,4254,4256,4259,4261,4264,4266,4269,4271,4273],{"class":119,"line":175},[117,4255,344],{"class":123},[117,4257,4258],{"class":257}," parsed",[117,4260,350],{"class":123},[117,4262,4263],{"class":130}," parsePhoneNumberFromString",[117,4265,199],{"class":134},[117,4267,4268],{"class":168},"`+${",[117,4270,4201],{"class":134},[117,4272,692],{"class":168},[117,4274,976],{"class":134},[117,4276,4277,4279,4282,4285],{"class":119,"line":181},[117,4278,3914],{"class":123},[117,4280,4281],{"class":134}," (parsed?.country ",[117,4283,4284],{"class":123},"&&",[117,4286,4287],{"class":134}," parsed.countryCallingCode) {\n",[117,4289,4290,4293,4296,4298,4301],{"class":119,"line":193},[117,4291,4292],{"class":123},"      const",[117,4294,4295],{"class":257}," parsedCountry",[117,4297,350],{"class":123},[117,4299,4300],{"class":130}," getCountryByValue",[117,4302,4303],{"class":134},"(parsed.country);\n",[117,4305,4306,4308],{"class":119,"line":215},[117,4307,3929],{"class":123},[117,4309,4310],{"class":134}," (parsedCountry) {\n",[117,4312,4313,4316,4319,4321,4324,4327,4329],{"class":119,"line":221},[117,4314,4315],{"class":123},"        return",[117,4317,4318],{"class":134}," { country: parsedCountry, nationalNumber: ",[117,4320,3989],{"class":130},[117,4322,4323],{"class":134},"(parsed.nationalNumber ",[117,4325,4326],{"class":123},"??",[117,4328,3937],{"class":168},[117,4330,4331],{"class":134},") };\n",[117,4333,4334],{"class":119,"line":230},[117,4335,4336],{"class":134},"      }\n",[117,4338,4339],{"class":119,"line":242},[117,4340,3951],{"class":134},[117,4342,4343,4346,4349],{"class":119,"line":285},[117,4344,4345],{"class":134},"  } ",[117,4347,4348],{"class":123},"catch",[117,4350,410],{"class":134},[117,4352,4353],{"class":119,"line":291},[117,4354,4355],{"class":187},"    \u002F* libphonenumber throws on partial input, fall through *\u002F\n",[117,4357,4358],{"class":119,"line":298},[117,4359,1097],{"class":134},[117,4361,4362],{"class":119,"line":304},[117,4363,295],{"emptyLinePlaceholder":294},[117,4365,4366],{"class":119,"line":330},[117,4367,4368],{"class":187},"  \u002F\u002F Tier 2: national-format parse using the silently-inferred country as a hint.\n",[117,4370,4371,4373,4376,4378],{"class":119,"line":335},[117,4372,506],{"class":123},[117,4374,4375],{"class":257}," hint",[117,4377,350],{"class":123},[117,4379,4380],{"class":134}," inferredCountry.value;\n",[117,4382,4383,4385,4388,4390,4393,4396,4399,4402],{"class":119,"line":341},[117,4384,1417],{"class":123},[117,4386,4387],{"class":134}," (hint ",[117,4389,4284],{"class":123},[117,4391,4392],{"class":134}," digits.",[117,4394,4395],{"class":257},"length",[117,4397,4398],{"class":123}," >=",[117,4400,4401],{"class":257}," 4",[117,4403,212],{"class":134},[117,4405,4406,4409],{"class":119,"line":358},[117,4407,4408],{"class":123},"    try",[117,4410,410],{"class":134},[117,4412,4413,4415,4417,4419,4421,4424,4427,4430],{"class":119,"line":368},[117,4414,4292],{"class":123},[117,4416,4258],{"class":257},[117,4418,350],{"class":123},[117,4420,4263],{"class":130},[117,4422,4423],{"class":134},"(digits, hint ",[117,4425,4426],{"class":123},"as",[117,4428,4429],{"class":130}," CountryCode",[117,4431,976],{"class":134},[117,4433,4434,4436,4439,4442],{"class":119,"line":380},[117,4435,3929],{"class":123},[117,4437,4438],{"class":134}," (parsed?.",[117,4440,4441],{"class":130},"isValid",[117,4443,4444],{"class":134},"()) {\n",[117,4446,4447,4450,4453,4455,4457,4460,4463],{"class":119,"line":385},[117,4448,4449],{"class":123},"        const",[117,4451,4452],{"class":257}," matched",[117,4454,350],{"class":123},[117,4456,4300],{"class":130},[117,4458,4459],{"class":134},"(parsed.country ",[117,4461,4462],{"class":123},"||",[117,4464,4465],{"class":134}," hint);\n",[117,4467,4468,4471],{"class":119,"line":413},[117,4469,4470],{"class":123},"        if",[117,4472,4473],{"class":134}," (matched) {\n",[117,4475,4476,4479,4482,4484,4486,4488,4490],{"class":119,"line":425},[117,4477,4478],{"class":123},"          return",[117,4480,4481],{"class":134}," { country: matched, nationalNumber: ",[117,4483,3989],{"class":130},[117,4485,4323],{"class":134},[117,4487,4326],{"class":123},[117,4489,3937],{"class":168},[117,4491,4331],{"class":134},[117,4493,4494],{"class":119,"line":430},[117,4495,4496],{"class":134},"        }\n",[117,4498,4499],{"class":119,"line":435},[117,4500,4336],{"class":134},[117,4502,4504,4507,4509],{"class":119,"line":4503},28,[117,4505,4506],{"class":134},"    } ",[117,4508,4348],{"class":123},[117,4510,410],{"class":134},[117,4512,4514],{"class":119,"line":4513},29,[117,4515,4516],{"class":187},"      \u002F* fall through *\u002F\n",[117,4518,4520],{"class":119,"line":4519},30,[117,4521,3951],{"class":134},[117,4523,4525],{"class":119,"line":4524},31,[117,4526,1097],{"class":134},[117,4528,4530],{"class":119,"line":4529},32,[117,4531,295],{"emptyLinePlaceholder":294},[117,4533,4535],{"class":119,"line":4534},33,[117,4536,4537],{"class":187},"  \u002F\u002F Tier 3: longest-prefix match over our own dial-digits index.\n",[117,4539,4541,4544,4546,4549,4552,4554,4557,4560,4562,4565,4568,4570,4573,4576,4579,4582,4585],{"class":119,"line":4540},34,[117,4542,4543],{"class":123},"  for",[117,4545,1359],{"class":134},[117,4547,4548],{"class":123},"let",[117,4550,4551],{"class":134}," len ",[117,4553,683],{"class":123},[117,4555,4556],{"class":134}," Math.",[117,4558,4559],{"class":130},"min",[117,4561,199],{"class":134},[117,4563,4564],{"class":257},"3",[117,4566,4567],{"class":134},", digits.",[117,4569,4395],{"class":257},[117,4571,4572],{"class":134},"); len ",[117,4574,4575],{"class":123},">=",[117,4577,4578],{"class":257}," 1",[117,4580,4581],{"class":134},"; len",[117,4583,4584],{"class":123},"--",[117,4586,212],{"class":134},[117,4588,4590,4592,4595,4597,4599,4602,4604,4607],{"class":119,"line":4589},35,[117,4591,344],{"class":123},[117,4593,4594],{"class":257}," prefix",[117,4596,350],{"class":123},[117,4598,4392],{"class":134},[117,4600,4601],{"class":130},"slice",[117,4603,199],{"class":134},[117,4605,4606],{"class":257},"0",[117,4608,4609],{"class":134},", len);\n",[117,4611,4613,4615,4618,4620,4623],{"class":119,"line":4612},36,[117,4614,344],{"class":123},[117,4616,4617],{"class":257}," group",[117,4619,350],{"class":123},[117,4621,4622],{"class":130}," getCountriesByDial",[117,4624,4625],{"class":134},"(prefix);\n",[117,4627,4629,4631,4633,4635,4638,4640,4642,4645],{"class":119,"line":4628},37,[117,4630,3914],{"class":123},[117,4632,1359],{"class":134},[117,4634,1589],{"class":123},[117,4636,4637],{"class":134},"group.",[117,4639,4395],{"class":257},[117,4641,671],{"class":134},[117,4643,4644],{"class":123},"continue",[117,4646,527],{"class":134},[117,4648,4650,4652,4655,4657,4659,4661,4664,4666],{"class":119,"line":4649},38,[117,4651,344],{"class":123},[117,4653,4654],{"class":257}," nationalNumber",[117,4656,350],{"class":123},[117,4658,4392],{"class":134},[117,4660,4601],{"class":130},[117,4662,4663],{"class":134},"(prefix.",[117,4665,4395],{"class":257},[117,4667,976],{"class":134},[117,4669,4671,4673,4676,4678,4681,4683,4685,4687,4690,4692],{"class":119,"line":4670},39,[117,4672,3914],{"class":123},[117,4674,4675],{"class":134}," (group.",[117,4677,4395],{"class":257},[117,4679,4680],{"class":123}," ===",[117,4682,4578],{"class":257},[117,4684,671],{"class":134},[117,4686,1423],{"class":123},[117,4688,4689],{"class":134}," { country: group[",[117,4691,4606],{"class":257},[117,4693,4694],{"class":134},"], nationalNumber };\n",[117,4696,4698],{"class":119,"line":4697},40,[117,4699,4700],{"class":187},"    \u002F\u002F ambiguity: prefer current selection, then recents, then first\n",[117,4702,4704],{"class":119,"line":4703},41,[117,4705,4706],{"class":187},"    \u002F\u002F (see \"Recents as the tiebreaker\" below)\n",[117,4708,4710,4712,4714,4716],{"class":119,"line":4709},42,[117,4711,1059],{"class":123},[117,4713,4689],{"class":134},[117,4715,4606],{"class":257},[117,4717,4694],{"class":134},[117,4719,4721],{"class":119,"line":4720},43,[117,4722,1097],{"class":134},[117,4724,4726,4728,4730],{"class":119,"line":4725},44,[117,4727,1498],{"class":123},[117,4729,1082],{"class":257},[117,4731,527],{"class":134},[117,4733,4735],{"class":119,"line":4734},45,[117,4736,589],{"class":134},[10,4738,4739],{},"The tiers exist because each handles a class of input the other tiers can't.",[10,4741,4742,4743,4745,4746,4749,4750,4753,4754,4757],{},"Tier 1 takes whatever the user typed, prepends ",[29,4744,4168],{},", and asks libphonenumber to parse it as an international number. If the user typed ",[29,4747,4748],{},"447911123456",", this matches the UK. If they typed ",[29,4751,4752],{},"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. ",[29,4755,4756],{},"447"," isn't a complete number; parsing it raises, and we fall through.",[10,4759,4760,4761,4763,4764,4767,4768,4771,4772,4774,4775,4777,4778,4781,4782,4785],{},"Tier 2 is the path for users who type the number they actually use locally. ",[29,4762,4172],{}," doesn't start with a dial code, so Tier 1 can't help. But if we ",[443,4765,4766],{},"silently"," know the user is probably in Egypt (from IP or timezone; more on that next), we can pass ",[29,4769,4770],{},"\"EG\""," as a parsing hint and libphonenumber will recognise ",[29,4773,4172],{}," as a valid Egyptian mobile number, strip the leading ",[29,4776,4606],{}," (Egypt's national prefix), and hand back the canonical national significant number ",[29,4779,4780],{},"1066105963",". The ",[29,4783,4784],{},"digits.length >= 4"," guard prevents the parser from treating two-digit area-code starts as full numbers.",[10,4787,4788,4789,4792,4793,4796,4797,4800],{},"Tier 3 is the fallback for \"you typed ",[29,4790,4791],{},"44"," and nothing else.\" Neither libphonenumber pass can match; the input is too short. But our own dial-digits index can: ",[29,4794,4795],{},"byDialDigits.get('44')"," returns the UK group, length 1, done. The bucketed-",[29,4798,4799],{},"Map\u003Cstring, CountryOption[]>"," shape from the old version earns its keep here.",[10,4802,4803],{},"Three tiers feels like a lot. It's the minimum: each tier handles inputs the other two reject.",[10,4805,4806],{},"The handler that calls into it stays small:",[108,4808,4810],{"className":110,"code":4809,"language":112,"meta":113,"style":113},"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",[29,4811,4812,4826,4835,4850,4866,4878,4893,4907,4922,4933,4943,4953,4957,4978],{"__ignoreMap":113},[117,4813,4814,4816,4819,4821,4824],{"class":119,"line":120},[117,4815,616],{"class":123},[117,4817,4818],{"class":257}," detectAndApply",[117,4820,350],{"class":123},[117,4822,4823],{"class":130}," useDebounceFn",[117,4825,966],{"class":134},[117,4827,4828,4831,4833],{"class":119,"line":144},[117,4829,4830],{"class":134},"  () ",[117,4832,251],{"class":123},[117,4834,410],{"class":134},[117,4836,4837,4839,4841,4843,4846,4848],{"class":119,"line":150},[117,4838,3914],{"class":123},[117,4840,1359],{"class":134},[117,4842,1589],{"class":123},[117,4844,4845],{"class":134},"props.detectFromInput) ",[117,4847,1423],{"class":123},[117,4849,527],{"class":134},[117,4851,4852,4854,4857,4859,4862,4864],{"class":119,"line":156},[117,4853,3914],{"class":123},[117,4855,4856],{"class":134}," (userPickedCountry.value ",[117,4858,4462],{"class":123},[117,4860,4861],{"class":134}," selectedIso2.value) ",[117,4863,1423],{"class":123},[117,4865,527],{"class":134},[117,4867,4868,4870,4873,4875],{"class":119,"line":162},[117,4869,344],{"class":123},[117,4871,4872],{"class":257}," current",[117,4874,350],{"class":123},[117,4876,4877],{"class":134}," phone.value;\n",[117,4879,4880,4882,4884,4886,4889,4891],{"class":119,"line":175},[117,4881,3914],{"class":123},[117,4883,1359],{"class":134},[117,4885,1589],{"class":123},[117,4887,4888],{"class":134},"current) ",[117,4890,1423],{"class":123},[117,4892,527],{"class":134},[117,4894,4895,4897,4900,4902,4904],{"class":119,"line":181},[117,4896,344],{"class":123},[117,4898,4899],{"class":257}," match",[117,4901,350],{"class":123},[117,4903,4196],{"class":130},[117,4905,4906],{"class":134},"(current);\n",[117,4908,4909,4911,4913,4915,4918,4920],{"class":119,"line":193},[117,4910,3914],{"class":123},[117,4912,1359],{"class":134},[117,4914,1589],{"class":123},[117,4916,4917],{"class":134},"match) ",[117,4919,1423],{"class":123},[117,4921,527],{"class":134},[117,4923,4924,4927,4929,4931],{"class":119,"line":215},[117,4925,4926],{"class":134},"    autoSettingCountry.value ",[117,4928,683],{"class":123},[117,4930,1017],{"class":257},[117,4932,527],{"class":134},[117,4934,4935,4938,4940],{"class":119,"line":221},[117,4936,4937],{"class":134},"    selectedIso2.value ",[117,4939,683],{"class":123},[117,4941,4942],{"class":134}," match.country.value;\n",[117,4944,4945,4948,4950],{"class":119,"line":230},[117,4946,4947],{"class":134},"    phone.value ",[117,4949,683],{"class":123},[117,4951,4952],{"class":134}," match.nationalNumber;\n",[117,4954,4955],{"class":119,"line":242},[117,4956,178],{"class":134},[117,4958,4959,4962,4964,4966,4968,4971,4973,4975],{"class":119,"line":285},[117,4960,4961],{"class":130},"  computed",[117,4963,2467],{"class":134},[117,4965,251],{"class":123},[117,4967,4556],{"class":134},[117,4969,4970],{"class":130},"max",[117,4972,199],{"class":134},[117,4974,4606],{"class":257},[117,4976,4977],{"class":134},", props.detectDebounceMs))\n",[117,4979,4980],{"class":119,"line":291},[117,4981,976],{"class":134},[10,4983,4984,3024,4987,4990,4991,4994,4995,4998],{},[29,4985,4986],{},"useDebounceFn",[29,4988,4989],{},"phone.value"," at the time the timer fires, not at schedule time, so a burst of keystrokes collapses into one parse. ",[29,4992,4993],{},"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 ",[443,4996,4997],{},"not"," debounced; it reverts the picker to hidden immediately:",[108,5000,5002],{"className":110,"code":5001,"language":112,"meta":113,"style":113},"if (!cleaned) {\n  autoSettingCountry.value = true;\n  selectedIso2.value = '';\n  phone.value = '';\n  userPickedCountry.value = false;\n  return;\n}\n",[29,5003,5004,5016,5027,5038,5049,5061,5067],{"__ignoreMap":113},[117,5005,5006,5009,5011,5013],{"class":119,"line":120},[117,5007,5008],{"class":123},"if",[117,5010,1359],{"class":134},[117,5012,1589],{"class":123},[117,5014,5015],{"class":134},"cleaned) {\n",[117,5017,5018,5021,5023,5025],{"class":119,"line":144},[117,5019,5020],{"class":134},"  autoSettingCountry.value ",[117,5022,683],{"class":123},[117,5024,1017],{"class":257},[117,5026,527],{"class":134},[117,5028,5029,5032,5034,5036],{"class":119,"line":150},[117,5030,5031],{"class":134},"  selectedIso2.value ",[117,5033,683],{"class":123},[117,5035,3937],{"class":168},[117,5037,527],{"class":134},[117,5039,5040,5043,5045,5047],{"class":119,"line":156},[117,5041,5042],{"class":134},"  phone.value ",[117,5044,683],{"class":123},[117,5046,3937],{"class":168},[117,5048,527],{"class":134},[117,5050,5051,5054,5056,5059],{"class":119,"line":162},[117,5052,5053],{"class":134},"  userPickedCountry.value ",[117,5055,683],{"class":123},[117,5057,5058],{"class":257}," false",[117,5060,527],{"class":134},[117,5062,5063,5065],{"class":119,"line":175},[117,5064,1498],{"class":123},[117,5066,527],{"class":134},[117,5068,5069],{"class":119,"line":181},[117,5070,589],{"class":134},[10,5072,5073],{},"Debouncing the clear would mean the dropdown lingers for 150ms after the input is empty, which looks wrong every time.",[22,5075,5077],{"id":5076},"the-environment-detection-chain-and-why-its-pluggable","The environment-detection chain (and why it's pluggable)",[10,5079,5080,5081,5084],{},"Tier 2 above depends on ",[29,5082,5083],{},"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,5086,5087,5088,512],{},"The new chain is ",[29,5089,5090],{},"detectCountry()",[108,5092,5094],{"className":110,"code":5093,"language":112,"meta":113,"style":113},"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",[29,5095,5096,5135,5141,5153,5165,5177,5189,5200,5209,5213,5220,5234,5244,5248,5252,5266,5278,5282,5286,5298,5315,5322,5335,5342,5346,5350,5354,5376,5397,5408,5415],{"__ignoreMap":113},[117,5097,5098,5100,5102,5105,5108,5110,5113,5115,5118,5120,5123,5125,5128,5130,5132],{"class":119,"line":120},[117,5099,124],{"class":123},[117,5101,1356],{"class":123},[117,5103,5104],{"class":123}," function",[117,5106,5107],{"class":130}," detectCountry",[117,5109,199],{"class":134},[117,5111,5112],{"class":202},"opts",[117,5114,512],{"class":123},[117,5116,5117],{"class":130}," DetectCountryOptions",[117,5119,350],{"class":123},[117,5121,5122],{"class":134}," {})",[117,5124,512],{"class":123},[117,5126,5127],{"class":130}," Promise",[117,5129,135],{"class":134},[117,5131,3691],{"class":257},[117,5133,5134],{"class":134},"> {\n",[117,5136,5137,5139],{"class":119,"line":144},[117,5138,506],{"class":123},[117,5140,410],{"class":134},[117,5142,5143,5146,5148,5151],{"class":119,"line":150},[117,5144,5145],{"class":257},"    strategy",[117,5147,350],{"class":123},[117,5149,5150],{"class":168}," 'auto'",[117,5152,239],{"class":134},[117,5154,5155,5158,5160,5163],{"class":119,"line":156},[117,5156,5157],{"class":257},"    ipEndpoint",[117,5159,350],{"class":123},[117,5161,5162],{"class":168}," 'https:\u002F\u002Fipapi.co\u002Fjson\u002F'",[117,5164,239],{"class":134},[117,5166,5167,5170,5172,5175],{"class":119,"line":162},[117,5168,5169],{"class":257},"    defaultCountry",[117,5171,350],{"class":123},[117,5173,5174],{"class":168}," 'US'",[117,5176,239],{"class":134},[117,5178,5179,5182,5184,5187],{"class":119,"line":175},[117,5180,5181],{"class":257},"    timeoutMs",[117,5183,350],{"class":123},[117,5185,5186],{"class":257}," 2000",[117,5188,239],{"class":134},[117,5190,5191,5194,5196,5198],{"class":119,"line":181},[117,5192,5193],{"class":257},"    cache",[117,5195,350],{"class":123},[117,5197,1017],{"class":257},[117,5199,239],{"class":134},[117,5201,5202,5204,5206],{"class":119,"line":193},[117,5203,4345],{"class":134},[117,5205,683],{"class":123},[117,5207,5208],{"class":134}," opts;\n",[117,5210,5211],{"class":119,"line":215},[117,5212,295],{"emptyLinePlaceholder":294},[117,5214,5215,5217],{"class":119,"line":221},[117,5216,1417],{"class":123},[117,5218,5219],{"class":134}," (cache) {\n",[117,5221,5222,5224,5226,5228,5231],{"class":119,"line":230},[117,5223,344],{"class":123},[117,5225,1401],{"class":257},[117,5227,350],{"class":123},[117,5229,5230],{"class":130}," readCache",[117,5232,5233],{"class":134},"();\n",[117,5235,5236,5238,5240,5242],{"class":119,"line":242},[117,5237,3914],{"class":123},[117,5239,1420],{"class":134},[117,5241,1423],{"class":123},[117,5243,1426],{"class":134},[117,5245,5246],{"class":119,"line":285},[117,5247,1097],{"class":134},[117,5249,5250],{"class":119,"line":291},[117,5251,295],{"emptyLinePlaceholder":294},[117,5253,5254,5256,5259,5261,5264],{"class":119,"line":298},[117,5255,1417],{"class":123},[117,5257,5258],{"class":134}," (strategy ",[117,5260,1068],{"class":123},[117,5262,5263],{"class":168}," 'none'",[117,5265,212],{"class":134},[117,5267,5268,5270,5273,5276],{"class":119,"line":304},[117,5269,1059],{"class":123},[117,5271,5272],{"class":134}," defaultCountry.",[117,5274,5275],{"class":130},"toUpperCase",[117,5277,5233],{"class":134},[117,5279,5280],{"class":119,"line":330},[117,5281,1097],{"class":134},[117,5283,5284],{"class":119,"line":335},[117,5285,295],{"emptyLinePlaceholder":294},[117,5287,5288,5290,5292,5294,5296],{"class":119,"line":341},[117,5289,1417],{"class":123},[117,5291,5258],{"class":134},[117,5293,1068],{"class":123},[117,5295,5150],{"class":168},[117,5297,212],{"class":134},[117,5299,5300,5302,5305,5307,5309,5312],{"class":119,"line":358},[117,5301,344],{"class":123},[117,5303,5304],{"class":257}," ipResult",[117,5306,350],{"class":123},[117,5308,624],{"class":123},[117,5310,5311],{"class":130}," tryIp",[117,5313,5314],{"class":134},"(ipEndpoint, timeoutMs);\n",[117,5316,5317,5319],{"class":119,"line":368},[117,5318,3914],{"class":123},[117,5320,5321],{"class":134}," (ipResult) {\n",[117,5323,5324,5326,5329,5332],{"class":119,"line":380},[117,5325,3929],{"class":123},[117,5327,5328],{"class":134}," (cache) ",[117,5330,5331],{"class":130},"writeCache",[117,5333,5334],{"class":134},"(ipResult);\n",[117,5336,5337,5339],{"class":119,"line":385},[117,5338,3944],{"class":123},[117,5340,5341],{"class":134}," ipResult;\n",[117,5343,5344],{"class":119,"line":413},[117,5345,3951],{"class":134},[117,5347,5348],{"class":119,"line":425},[117,5349,1097],{"class":134},[117,5351,5352],{"class":119,"line":430},[117,5353,295],{"emptyLinePlaceholder":294},[117,5355,5356,5358,5361,5363,5366,5369,5371,5374],{"class":119,"line":435},[117,5357,506],{"class":123},[117,5359,5360],{"class":257}," localResult",[117,5362,350],{"class":123},[117,5364,5365],{"class":130}," tryTimezone",[117,5367,5368],{"class":134},"() ",[117,5370,4326],{"class":123},[117,5372,5373],{"class":130}," tryLocale",[117,5375,5233],{"class":134},[117,5377,5378,5380,5383,5385,5388,5390,5393,5395],{"class":119,"line":4503},[117,5379,506],{"class":123},[117,5381,5382],{"class":257}," final",[117,5384,350],{"class":123},[117,5386,5387],{"class":134}," (localResult ",[117,5389,4326],{"class":123},[117,5391,5392],{"class":134}," defaultCountry).",[117,5394,5275],{"class":130},[117,5396,5233],{"class":134},[117,5398,5399,5401,5403,5405],{"class":119,"line":4513},[117,5400,1417],{"class":123},[117,5402,5328],{"class":134},[117,5404,5331],{"class":130},[117,5406,5407],{"class":134},"(final);\n",[117,5409,5410,5412],{"class":119,"line":4519},[117,5411,1498],{"class":123},[117,5413,5414],{"class":134}," final;\n",[117,5416,5417],{"class":119,"line":4524},[117,5418,589],{"class":134},[10,5420,5421],{},"Four signals, evaluated in cost order: cheap-and-cached first, expensive-and-networked last.",[456,5423,5424,5430,5441,5468,5482],{},[36,5425,5426,5429],{},[29,5427,5428],{},"sessionStorage"," cache. The user already resolved to a country during this session; we don't redo the work.",[36,5431,5432,5433,5436,5437,5440],{},"IP geolocation via ",[29,5434,5435],{},"ipapi.co"," (configurable). The most accurate signal but it costs a network call and a tiny bit of privacy. ",[29,5438,5439],{},"strategy: 'locale'"," skips this step for consumers who don't want the request.",[36,5442,5443,5444,5447,5448,5451,5452,206,5455,5451,5458,206,5461,5451,5464,5467],{},"Timezone, via ",[29,5445,5446],{},"Intl.DateTimeFormat().resolvedOptions().timeZone",", looked up against a hand-rolled table that maps ",[29,5449,5450],{},"Africa\u002FCairo"," to ",[29,5453,5454],{},"EG",[29,5456,5457],{},"Asia\u002FRiyadh",[29,5459,5460],{},"SA",[29,5462,5463],{},"Europe\u002FLondon",[29,5465,5466],{},"GB",", and so on. The map covers the most-populated zones; it's honest about not covering every one. A miss falls through.",[36,5469,5470,5473,5474,5477,5478,5481],{},[29,5471,5472],{},"navigator.language",", which gives ",[29,5475,5476],{},"en-EG"," or ",[29,5479,5480],{},"ar-SA"," and similar. The region suffix is the country. Less reliable (users move; browser locales don't), but free.",[36,5483,5484,5485,5488],{},"The caller-supplied default. ",[29,5486,5487],{},"defaultCountry: 'US'"," is the library default; consumers can pick their own.",[10,5490,5491],{},"The IP step is the one that needed real care, because the call can hang:",[108,5493,5495],{"className":110,"code":5494,"language":112,"meta":113,"style":113},"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",[29,5496,5497,5540,5573,5590,5615,5621,5643,5660,5705,5738,5773,5781,5789,5798,5806,5810],{"__ignoreMap":113},[117,5498,5499,5502,5504,5506,5508,5511,5513,5515,5517,5520,5522,5524,5526,5528,5530,5532,5534,5536,5538],{"class":119,"line":120},[117,5500,5501],{"class":123},"async",[117,5503,5104],{"class":123},[117,5505,5311],{"class":130},[117,5507,199],{"class":134},[117,5509,5510],{"class":202},"endpoint",[117,5512,512],{"class":123},[117,5514,524],{"class":257},[117,5516,206],{"class":134},[117,5518,5519],{"class":202},"timeoutMs",[117,5521,512],{"class":123},[117,5523,537],{"class":257},[117,5525,271],{"class":134},[117,5527,512],{"class":123},[117,5529,5127],{"class":130},[117,5531,135],{"class":134},[117,5533,3691],{"class":257},[117,5535,1556],{"class":123},[117,5537,1082],{"class":257},[117,5539,5134],{"class":134},[117,5541,5542,5544,5546,5548,5551,5553,5555,5557,5560,5562,5565,5567,5569,5571],{"class":119,"line":144},[117,5543,1417],{"class":123},[117,5545,1359],{"class":134},[117,5547,1589],{"class":123},[117,5549,5550],{"class":130},"isBrowser",[117,5552,5368],{"class":134},[117,5554,4462],{"class":123},[117,5556,1062],{"class":123},[117,5558,5559],{"class":134}," fetch ",[117,5561,1079],{"class":123},[117,5563,5564],{"class":168}," 'function'",[117,5566,671],{"class":134},[117,5568,1423],{"class":123},[117,5570,1082],{"class":257},[117,5572,527],{"class":134},[117,5574,5575,5577,5580,5582,5585,5588],{"class":119,"line":150},[117,5576,506],{"class":123},[117,5578,5579],{"class":257}," controller",[117,5581,350],{"class":123},[117,5583,5584],{"class":123}," new",[117,5586,5587],{"class":130}," AbortController",[117,5589,5233],{"class":134},[117,5591,5592,5594,5597,5599,5602,5604,5606,5609,5612],{"class":119,"line":156},[117,5593,506],{"class":123},[117,5595,5596],{"class":257}," timer",[117,5598,350],{"class":123},[117,5600,5601],{"class":130}," setTimeout",[117,5603,2467],{"class":134},[117,5605,251],{"class":123},[117,5607,5608],{"class":134}," controller.",[117,5610,5611],{"class":130},"abort",[117,5613,5614],{"class":134},"(), timeoutMs);\n",[117,5616,5617,5619],{"class":119,"line":162},[117,5618,4249],{"class":123},[117,5620,410],{"class":134},[117,5622,5623,5625,5628,5630,5632,5635,5638,5641],{"class":119,"line":175},[117,5624,344],{"class":123},[117,5626,5627],{"class":257}," res",[117,5629,350],{"class":123},[117,5631,624],{"class":123},[117,5633,5634],{"class":130}," fetch",[117,5636,5637],{"class":134},"(endpoint, { signal: controller.signal, credentials: ",[117,5639,5640],{"class":168},"'omit'",[117,5642,327],{"class":134},[117,5644,5645,5647,5649,5651,5654,5656,5658],{"class":119,"line":181},[117,5646,3914],{"class":123},[117,5648,1359],{"class":134},[117,5650,1589],{"class":123},[117,5652,5653],{"class":134},"res.ok) ",[117,5655,1423],{"class":123},[117,5657,1082],{"class":257},[117,5659,527],{"class":134},[117,5661,5662,5664,5667,5669,5671,5674,5677,5680,5683,5685,5687,5690,5693,5695,5697,5699,5701,5703],{"class":119,"line":193},[117,5663,344],{"class":123},[117,5665,5666],{"class":257}," data",[117,5668,350],{"class":123},[117,5670,1359],{"class":134},[117,5672,5673],{"class":123},"await",[117,5675,5676],{"class":134}," res.",[117,5678,5679],{"class":130},"json",[117,5681,5682],{"class":134},"()) ",[117,5684,4426],{"class":123},[117,5686,549],{"class":134},[117,5688,5689],{"class":202},"country_code",[117,5691,5692],{"class":123},"?:",[117,5694,524],{"class":257},[117,5696,559],{"class":134},[117,5698,3734],{"class":202},[117,5700,5692],{"class":123},[117,5702,524],{"class":257},[117,5704,569],{"class":134},[117,5706,5707,5709,5712,5714,5717,5719,5722,5724,5726,5728,5731,5734,5736],{"class":119,"line":215},[117,5708,344],{"class":123},[117,5710,5711],{"class":257}," code",[117,5713,350],{"class":123},[117,5715,5716],{"class":134}," (data.country_code ",[117,5718,4326],{"class":123},[117,5720,5721],{"class":134}," data.country ",[117,5723,4326],{"class":123},[117,5725,3937],{"class":168},[117,5727,3043],{"class":134},[117,5729,5730],{"class":130},"toString",[117,5732,5733],{"class":134},"().",[117,5735,5275],{"class":130},[117,5737,5233],{"class":134},[117,5739,5740,5742,5744,5746,5749,5752,5754,5756,5759,5762,5764,5767,5769,5771],{"class":119,"line":221},[117,5741,1059],{"class":123},[117,5743,2908],{"class":168},[117,5745,1207],{"class":123},[117,5747,5748],{"class":257},"[A-Z]",[117,5750,5751],{"class":123},"{2}$",[117,5753,1204],{"class":168},[117,5755,17],{"class":134},[117,5757,5758],{"class":130},"test",[117,5760,5761],{"class":134},"(code) ",[117,5763,1370],{"class":123},[117,5765,5766],{"class":134}," code ",[117,5768,512],{"class":123},[117,5770,1082],{"class":257},[117,5772,527],{"class":134},[117,5774,5775,5777,5779],{"class":119,"line":230},[117,5776,4345],{"class":134},[117,5778,4348],{"class":123},[117,5780,410],{"class":134},[117,5782,5783,5785,5787],{"class":119,"line":242},[117,5784,1059],{"class":123},[117,5786,1082],{"class":257},[117,5788,527],{"class":134},[117,5790,5791,5793,5796],{"class":119,"line":285},[117,5792,4345],{"class":134},[117,5794,5795],{"class":123},"finally",[117,5797,410],{"class":134},[117,5799,5800,5803],{"class":119,"line":291},[117,5801,5802],{"class":130},"    clearTimeout",[117,5804,5805],{"class":134},"(timer);\n",[117,5807,5808],{"class":119,"line":298},[117,5809,1097],{"class":134},[117,5811,5812],{"class":119,"line":304},[117,5813,589],{"class":134},[10,5815,5816,5819,5820,5822,5823,5826,5827,5830,5831,5834,5835,5834,5838,5840],{},[29,5817,5818],{},"AbortController"," plus a 2-second timer means a slow IP service can't block the detection chain for longer than ",[29,5821,5519],{},". ",[29,5824,5825],{},"credentials: 'omit'"," keeps cookies out of the request to whatever third party you've configured. The shape of the response is checked with ",[29,5828,5829],{},"\u002F^[A-Z]{2}$\u002F"," because some IP APIs return ",[29,5832,5833],{},"\"GB\"",", some return ",[29,5836,5837],{},"\"United Kingdom\"",[29,5839,2013],{}," when they don't know. The regex is the gate.",[10,5842,5843,5846],{},[29,5844,5845],{},"detectCountry"," lives outside the component on purpose:",[108,5848,5850],{"className":110,"code":5849,"language":112,"meta":113,"style":113},"import { detectCountry } from '@alikhalilll\u002Fui\u002Ftell-input';\n\nconst country = await detectCountry({ strategy: 'locale' });\n",[29,5851,5852,5866,5870],{"__ignoreMap":113},[117,5853,5854,5856,5859,5861,5864],{"class":119,"line":120},[117,5855,2140],{"class":123},[117,5857,5858],{"class":134}," { detectCountry } ",[117,5860,2152],{"class":123},[117,5862,5863],{"class":168}," '@alikhalilll\u002Fui\u002Ftell-input'",[117,5865,527],{"class":134},[117,5867,5868],{"class":119,"line":144},[117,5869,295],{"emptyLinePlaceholder":294},[117,5871,5872,5874,5876,5878,5880,5882,5885,5888],{"class":119,"line":150},[117,5873,616],{"class":123},[117,5875,3804],{"class":257},[117,5877,350],{"class":123},[117,5879,624],{"class":123},[117,5881,5107],{"class":130},[117,5883,5884],{"class":134},"({ strategy: ",[117,5886,5887],{"class":168},"'locale'",[117,5889,327],{"class":134},[10,5891,5892,5893,5896,5897,5900],{},"No Vue dependency, no reactive refs, no ",[29,5894,5895],{},"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 (",[29,5898,5899],{},"useCountryDetection",") is a thin shell around it for the in-component case. Two surfaces, one implementation.",[10,5902,5903],{},"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:",[108,5905,5907],{"className":1896,"code":5906,"language":1898,"meta":113,"style":113},"\u003CATellInput\n  v-model:phone=\"phone\"\n  v-model:country=\"country\"\n  :detector=\"(opts) => Promise.resolve(serverDetectedCountry.value)\"\n\u002F>\n",[29,5908,5909,5916,5935,5951,5981],{"__ignoreMap":113},[117,5910,5911,5913],{"class":119,"line":120},[117,5912,135],{"class":134},[117,5914,5915],{"class":1907},"ATellInput\n",[117,5917,5918,5921,5923,5926,5928,5930,5932],{"class":119,"line":144},[117,5919,5920],{"class":130},"  v-model",[117,5922,512],{"class":134},[117,5924,5925],{"class":130},"phone",[117,5927,683],{"class":134},[117,5929,1916],{"class":168},[117,5931,5925],{"class":134},[117,5933,5934],{"class":168},"\"\n",[117,5936,5937,5939,5941,5943,5945,5947,5949],{"class":119,"line":150},[117,5938,5920],{"class":130},[117,5940,512],{"class":134},[117,5942,3734],{"class":130},[117,5944,683],{"class":134},[117,5946,1916],{"class":168},[117,5948,3734],{"class":134},[117,5950,5934],{"class":168},[117,5952,5953,5955,5958,5960,5962,5964,5966,5968,5970,5972,5974,5976,5979],{"class":119,"line":156},[117,5954,820],{"class":134},[117,5956,5957],{"class":130},"detector",[117,5959,683],{"class":134},[117,5961,1916],{"class":168},[117,5963,199],{"class":134},[117,5965,5112],{"class":202},[117,5967,671],{"class":134},[117,5969,251],{"class":123},[117,5971,5127],{"class":257},[117,5973,17],{"class":134},[117,5975,313],{"class":130},[117,5977,5978],{"class":134},"(serverDetectedCountry.value)",[117,5980,5934],{"class":168},[117,5982,5983],{"class":119,"line":162},[117,5984,5985],{"class":134},"\u002F>\n",[10,5987,4025,5988,5990,5991,5994],{},[29,5989,5957],{}," 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 ",[443,5992,5993],{},"source"," of the answer becomes whatever you said it was.",[22,5996,5998],{"id":5997},"responsive-picker-one-component-two-presentations","Responsive picker: one component, two presentations",[10,6000,6001],{},"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,6003,6004,6005,6008],{},"The new picker uses the ",[29,6006,6007],{},"AResponsivePopover"," primitive from the same library, which is a popover on desktop and a vaul-vue drawer on mobile:",[108,6010,6012],{"className":1896,"code":6011,"language":1898,"meta":113,"style":113},"\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",[29,6013,6014,6038,6043,6048,6053,6057,6062,6067,6072,6077,6082,6087,6092,6097,6102,6107,6112],{"__ignoreMap":113},[117,6015,6016,6018,6020,6023,6025,6028,6030,6032,6034,6036],{"class":119,"line":120},[117,6017,135],{"class":134},[117,6019,6007],{"class":1907},[117,6021,6022],{"class":130}," v-model",[117,6024,512],{"class":134},[117,6026,6027],{"class":130},"open",[117,6029,683],{"class":134},[117,6031,1916],{"class":168},[117,6033,6027],{"class":134},[117,6035,1916],{"class":168},[117,6037,1924],{"class":134},[117,6039,6040],{"class":119,"line":144},[117,6041,6042],{"class":134},"  \u003CAResponsivePopoverTrigger as-child>\n",[117,6044,6045],{"class":119,"line":150},[117,6046,6047],{"class":134},"    \u003C!-- trigger button -->\n",[117,6049,6050],{"class":119,"line":156},[117,6051,6052],{"class":134},"  \u003C\u002FAResponsivePopoverTrigger>\n",[117,6054,6055],{"class":119,"line":162},[117,6056,295],{"emptyLinePlaceholder":294},[117,6058,6059],{"class":119,"line":175},[117,6060,6061],{"class":134},"  \u003CAResponsivePopoverContent\n",[117,6063,6064],{"class":119,"line":181},[117,6065,6066],{"class":134},"    :popover-class=\"\n",[117,6068,6069],{"class":119,"line":193},[117,6070,6071],{"class":134},"      cn(\n",[117,6073,6074],{"class":119,"line":215},[117,6075,6076],{"class":134},"        'w-[min(20rem,calc(100vw-2rem))] max-h-[min(22rem,var(--reka-popover-content-available-height))]',\n",[117,6078,6079],{"class":119,"line":221},[117,6080,6081],{"class":134},"        props.popoverClass\n",[117,6083,6084],{"class":119,"line":230},[117,6085,6086],{"class":134},"      )\n",[117,6088,6089],{"class":119,"line":242},[117,6090,6091],{"class":134},"    \"\n",[117,6093,6094],{"class":119,"line":285},[117,6095,6096],{"class":134},"    :drawer-class=\"cn('max-h-[80vh] pb-4', props.drawerClass)\"\n",[117,6098,6099],{"class":119,"line":291},[117,6100,6101],{"class":134},"  >\n",[117,6103,6104],{"class":119,"line":298},[117,6105,6106],{"class":134},"    \u003C!-- search + list -->\n",[117,6108,6109],{"class":119,"line":304},[117,6110,6111],{"class":134},"  \u003C\u002FAResponsivePopoverContent>\n",[117,6113,6114,6116,6118],{"class":119,"line":330},[117,6115,1939],{"class":134},[117,6117,6007],{"class":1907},[117,6119,1924],{"class":134},[10,6121,6122,6123,3086,6126,6129,6130,6133,6134,6137],{},"Two class props (",[29,6124,6125],{},"popoverClass",[29,6127,6128],{},"drawerClass",") get forwarded separately, so consumers can theme each presentation without the other inheriting it. The popover wants ",[29,6131,6132],{},"max-h-[min(22rem,var(--reka-popover-content-available-height))]"," because reka-ui exposes the available height as a CSS variable; the drawer wants ",[29,6135,6136],{},"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,6139,6140],{},"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.",[22,6142,6144],{"id":6143},"slots-for-everything-fourteen-of-them","Slots for everything (fourteen of them)",[10,6146,6147],{},"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,6149,6150],{},"The new version owns the behaviour and lets the consumer own the look:",[108,6152,6154],{"className":110,"code":6153,"language":112,"meta":113,"style":113},"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",[29,6155,6156,6164,6180,6231,6246,6276,6327,6373,6426,6455,6499,6560,6605,6666,6681,6711],{"__ignoreMap":113},[117,6157,6158,6161],{"class":119,"line":120},[117,6159,6160],{"class":130},"defineSlots",[117,6162,6163],{"class":134},"\u003C{\n",[117,6165,6166,6169,6171,6174,6176,6178],{"class":119,"line":144},[117,6167,6168],{"class":130},"  prefix",[117,6170,5692],{"class":123},[117,6172,6173],{"class":134}," () ",[117,6175,251],{"class":123},[117,6177,1041],{"class":257},[117,6179,527],{"class":134},[117,6181,6182,6185,6187,6189,6191,6193,6195,6198,6200,6203,6205,6208,6210,6213,6215,6218,6220,6223,6225,6227,6229],{"class":119,"line":150},[117,6183,6184],{"class":130},"  suffix",[117,6186,5692],{"class":123},[117,6188,1359],{"class":134},[117,6190,2544],{"class":202},[117,6192,512],{"class":123},[117,6194,549],{"class":134},[117,6196,6197],{"class":202},"validationState",[117,6199,512],{"class":123},[117,6201,6202],{"class":168}," 'idle'",[117,6204,1556],{"class":123},[117,6206,6207],{"class":168}," 'valid'",[117,6209,1556],{"class":123},[117,6211,6212],{"class":168}," 'error'",[117,6214,559],{"class":134},[117,6216,6217],{"class":202},"validation",[117,6219,512],{"class":123},[117,6221,6222],{"class":130}," PhoneValidationResult",[117,6224,405],{"class":134},[117,6226,251],{"class":123},[117,6228,1041],{"class":257},[117,6230,527],{"class":134},[117,6232,6233,6236,6238,6240,6242,6244],{"class":119,"line":156},[117,6234,6235],{"class":168},"  'valid-icon'",[117,6237,5692],{"class":123},[117,6239,6173],{"class":134},[117,6241,251],{"class":123},[117,6243,1041],{"class":257},[117,6245,527],{"class":134},[117,6247,6248,6251,6253,6255,6257,6259,6261,6264,6266,6268,6270,6272,6274],{"class":119,"line":162},[117,6249,6250],{"class":168},"  'error-icon'",[117,6252,5692],{"class":123},[117,6254,1359],{"class":134},[117,6256,2544],{"class":202},[117,6258,512],{"class":123},[117,6260,549],{"class":134},[117,6262,6263],{"class":202},"reason",[117,6265,512],{"class":123},[117,6267,524],{"class":257},[117,6269,405],{"class":134},[117,6271,251],{"class":123},[117,6273,1041],{"class":257},[117,6275,527],{"class":134},[117,6277,6278,6281,6283,6285,6287,6289,6291,6293,6295,6297,6299,6302,6304,6306,6308,6311,6313,6315,6317,6319,6321,6323,6325],{"class":119,"line":175},[117,6279,6280],{"class":130},"  hint",[117,6282,5692],{"class":123},[117,6284,1359],{"class":134},[117,6286,2544],{"class":202},[117,6288,512],{"class":123},[117,6290,549],{"class":134},[117,6292,3734],{"class":202},[117,6294,512],{"class":123},[117,6296,524],{"class":257},[117,6298,559],{"class":134},[117,6300,6301],{"class":202},"formatHint",[117,6303,512],{"class":123},[117,6305,524],{"class":257},[117,6307,559],{"class":134},[117,6309,6310],{"class":202},"example",[117,6312,512],{"class":123},[117,6314,524],{"class":257},[117,6316,1556],{"class":123},[117,6318,1082],{"class":257},[117,6320,405],{"class":134},[117,6322,251],{"class":123},[117,6324,1041],{"class":257},[117,6326,527],{"class":134},[117,6328,6329,6332,6334,6336,6338,6340,6342,6345,6347,6349,6351,6353,6355,6357,6359,6361,6363,6365,6367,6369,6371],{"class":119,"line":181},[117,6330,6331],{"class":130},"  error",[117,6333,5692],{"class":123},[117,6335,1359],{"class":134},[117,6337,2544],{"class":202},[117,6339,512],{"class":123},[117,6341,549],{"class":134},[117,6343,6344],{"class":202},"message",[117,6346,512],{"class":123},[117,6348,524],{"class":257},[117,6350,559],{"class":134},[117,6352,6263],{"class":202},[117,6354,512],{"class":123},[117,6356,524],{"class":257},[117,6358,559],{"class":134},[117,6360,6217],{"class":202},[117,6362,512],{"class":123},[117,6364,6222],{"class":130},[117,6366,405],{"class":134},[117,6368,251],{"class":123},[117,6370,1041],{"class":257},[117,6372,527],{"class":134},[117,6374,6375,6378,6380,6382,6384,6386,6388,6391,6393,6396,6398,6400,6402,6404,6406,6409,6411,6414,6416,6418,6420,6422,6424],{"class":119,"line":193},[117,6376,6377],{"class":130},"  trigger",[117,6379,5692],{"class":123},[117,6381,1359],{"class":134},[117,6383,2544],{"class":202},[117,6385,512],{"class":123},[117,6387,549],{"class":134},[117,6389,6390],{"class":202},"selectedCountry",[117,6392,512],{"class":123},[117,6394,6395],{"class":130}," CountryOption",[117,6397,1556],{"class":123},[117,6399,1082],{"class":257},[117,6401,559],{"class":134},[117,6403,6027],{"class":202},[117,6405,512],{"class":123},[117,6407,6408],{"class":257}," boolean",[117,6410,559],{"class":134},[117,6412,6413],{"class":202},"sizeClasses",[117,6415,512],{"class":123},[117,6417,524],{"class":257},[117,6419,405],{"class":134},[117,6421,251],{"class":123},[117,6423,1041],{"class":257},[117,6425,527],{"class":134},[117,6427,6428,6431,6433,6435,6437,6439,6441,6443,6445,6447,6449,6451,6453],{"class":119,"line":215},[117,6429,6430],{"class":130},"  chevron",[117,6432,5692],{"class":123},[117,6434,1359],{"class":134},[117,6436,2544],{"class":202},[117,6438,512],{"class":123},[117,6440,549],{"class":134},[117,6442,6027],{"class":202},[117,6444,512],{"class":123},[117,6446,6408],{"class":257},[117,6448,405],{"class":134},[117,6450,251],{"class":123},[117,6452,1041],{"class":257},[117,6454,527],{"class":134},[117,6456,6457,6460,6462,6464,6466,6468,6470,6472,6474,6476,6478,6481,6483,6486,6488,6491,6493,6495,6497],{"class":119,"line":221},[117,6458,6459],{"class":130},"  flag",[117,6461,5692],{"class":123},[117,6463,1359],{"class":134},[117,6465,2544],{"class":202},[117,6467,512],{"class":123},[117,6469,549],{"class":134},[117,6471,3734],{"class":202},[117,6473,512],{"class":123},[117,6475,6395],{"class":130},[117,6477,559],{"class":134},[117,6479,6480],{"class":202},"context",[117,6482,512],{"class":123},[117,6484,6485],{"class":168}," 'trigger'",[117,6487,1556],{"class":123},[117,6489,6490],{"class":168}," 'item'",[117,6492,405],{"class":134},[117,6494,251],{"class":123},[117,6496,1041],{"class":257},[117,6498,527],{"class":134},[117,6500,6501,6504,6506,6508,6510,6512,6514,6516,6518,6520,6522,6525,6527,6529,6531,6534,6536,6538,6540,6543,6545,6547,6549,6552,6554,6556,6558],{"class":119,"line":230},[117,6502,6503],{"class":130},"  item",[117,6505,5692],{"class":123},[117,6507,1359],{"class":134},[117,6509,2544],{"class":202},[117,6511,512],{"class":123},[117,6513,549],{"class":134},[117,6515,3734],{"class":202},[117,6517,512],{"class":123},[117,6519,6395],{"class":130},[117,6521,559],{"class":134},[117,6523,6524],{"class":202},"selected",[117,6526,512],{"class":123},[117,6528,6408],{"class":257},[117,6530,559],{"class":134},[117,6532,6533],{"class":202},"disabled",[117,6535,512],{"class":123},[117,6537,6408],{"class":257},[117,6539,559],{"class":134},[117,6541,6542],{"class":130},"select",[117,6544,512],{"class":123},[117,6546,6173],{"class":134},[117,6548,251],{"class":123},[117,6550,6551],{"class":257}," void",[117,6553,405],{"class":134},[117,6555,251],{"class":123},[117,6557,1041],{"class":257},[117,6559,527],{"class":134},[117,6561,6562,6565,6567,6569,6571,6573,6575,6578,6580,6582,6584,6587,6589,6592,6594,6597,6599,6601,6603],{"class":119,"line":242},[117,6563,6564],{"class":168},"  'group-header'",[117,6566,5692],{"class":123},[117,6568,1359],{"class":134},[117,6570,2544],{"class":202},[117,6572,512],{"class":123},[117,6574,549],{"class":134},[117,6576,6577],{"class":202},"label",[117,6579,512],{"class":123},[117,6581,524],{"class":257},[117,6583,559],{"class":134},[117,6585,6586],{"class":202},"group",[117,6588,512],{"class":123},[117,6590,6591],{"class":168}," 'suggested'",[117,6593,1556],{"class":123},[117,6595,6596],{"class":168}," 'all'",[117,6598,405],{"class":134},[117,6600,251],{"class":123},[117,6602,1041],{"class":257},[117,6604,527],{"class":134},[117,6606,6607,6610,6612,6614,6616,6618,6620,6623,6625,6627,6629,6632,6634,6636,6639,6641,6643,6645,6647,6649,6651,6654,6656,6658,6660,6662,6664],{"class":119,"line":285},[117,6608,6609],{"class":130},"  search",[117,6611,5692],{"class":123},[117,6613,1359],{"class":134},[117,6615,2544],{"class":202},[117,6617,512],{"class":123},[117,6619,549],{"class":134},[117,6621,6622],{"class":202},"value",[117,6624,512],{"class":123},[117,6626,524],{"class":257},[117,6628,559],{"class":134},[117,6630,6631],{"class":130},"setValue",[117,6633,512],{"class":123},[117,6635,1359],{"class":134},[117,6637,6638],{"class":202},"v",[117,6640,512],{"class":123},[117,6642,524],{"class":257},[117,6644,671],{"class":134},[117,6646,251],{"class":123},[117,6648,6551],{"class":257},[117,6650,559],{"class":134},[117,6652,6653],{"class":202},"isSearching",[117,6655,512],{"class":123},[117,6657,6408],{"class":257},[117,6659,405],{"class":134},[117,6661,251],{"class":123},[117,6663,1041],{"class":257},[117,6665,527],{"class":134},[117,6667,6668,6671,6673,6675,6677,6679],{"class":119,"line":291},[117,6669,6670],{"class":130},"  loading",[117,6672,5692],{"class":123},[117,6674,6173],{"class":134},[117,6676,251],{"class":123},[117,6678,1041],{"class":257},[117,6680,527],{"class":134},[117,6682,6683,6686,6688,6690,6692,6694,6696,6699,6701,6703,6705,6707,6709],{"class":119,"line":298},[117,6684,6685],{"class":130},"  empty",[117,6687,5692],{"class":123},[117,6689,1359],{"class":134},[117,6691,2544],{"class":202},[117,6693,512],{"class":123},[117,6695,549],{"class":134},[117,6697,6698],{"class":202},"query",[117,6700,512],{"class":123},[117,6702,524],{"class":257},[117,6704,405],{"class":134},[117,6706,251],{"class":123},[117,6708,1041],{"class":257},[117,6710,527],{"class":134},[117,6712,6713],{"class":119,"line":304},[117,6714,6715],{"class":134},"}>();\n",[10,6717,6718,6719,206,6722,6725,6726,206,6729,6732,6733,206,6736,206,6739,6742,6743,206,6746,206,6748,206,6751,206,6754,6757],{},"Fourteen slots, three groups. The visual ones (",[29,6720,6721],{},"prefix",[29,6723,6724],{},"suffix",", the two icon slots, ",[29,6727,6728],{},"hint",[29,6730,6731],{},"error",") decorate the field itself. The picker-trigger ones (",[29,6734,6735],{},"trigger",[29,6737,6738],{},"chevron",[29,6740,6741],{},"flag",") replace the button users click. The picker-content ones (",[29,6744,6745],{},"search",[29,6747,2102],{},[29,6749,6750],{},"empty",[29,6752,6753],{},"group-header",[29,6755,6756],{},"item",") replace anything inside the popover or drawer.",[10,6759,6760,6761,6764],{},"Almost all of the picker slots forward straight to ",[29,6762,6763],{},"ACountrySelect",". The component itself does the routing:",[108,6766,6768],{"className":1896,"code":6767,"language":1898,"meta":113,"style":113},"\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",[29,6769,6770,6803,6830,6838,6869,6892,6900],{"__ignoreMap":113},[117,6771,6772,6774,6776,6778,6780,6782,6785,6787,6790,6792,6794,6796,6799,6801],{"class":119,"line":120},[117,6773,135],{"class":134},[117,6775,2638],{"class":1907},[117,6777,1911],{"class":123},[117,6779,683],{"class":134},[117,6781,1916],{"class":168},[117,6783,6784],{"class":134},"$slots.trigger",[117,6786,1916],{"class":168},[117,6788,6789],{"class":134}," #",[117,6791,6735],{"class":130},[117,6793,683],{"class":134},[117,6795,1916],{"class":168},[117,6797,6798],{"class":134},"slotProps",[117,6800,1916],{"class":168},[117,6802,1924],{"class":134},[117,6804,6805,6807,6810,6813,6815,6818,6821,6823,6826,6828],{"class":119,"line":144},[117,6806,2645],{"class":134},[117,6808,6809],{"class":1907},"slot",[117,6811,6812],{"class":130}," name",[117,6814,683],{"class":134},[117,6816,6817],{"class":168},"\"trigger\"",[117,6819,6820],{"class":130}," v-bind",[117,6822,683],{"class":134},[117,6824,6825],{"class":168},"\"slotProps\"",[117,6827,2908],{"class":2907},[117,6829,1924],{"class":134},[117,6831,6832,6834,6836],{"class":119,"line":150},[117,6833,1939],{"class":134},[117,6835,2638],{"class":1907},[117,6837,1924],{"class":134},[117,6839,6840,6842,6844,6846,6848,6850,6853,6855,6857,6859,6861,6863,6865,6867],{"class":119,"line":156},[117,6841,135],{"class":134},[117,6843,2638],{"class":1907},[117,6845,1911],{"class":123},[117,6847,683],{"class":134},[117,6849,1916],{"class":168},[117,6851,6852],{"class":134},"$slots.chevron",[117,6854,1916],{"class":168},[117,6856,6789],{"class":134},[117,6858,6738],{"class":130},[117,6860,683],{"class":134},[117,6862,1916],{"class":168},[117,6864,6798],{"class":134},[117,6866,1916],{"class":168},[117,6868,1924],{"class":134},[117,6870,6871,6873,6875,6877,6879,6882,6884,6886,6888,6890],{"class":119,"line":162},[117,6872,2645],{"class":134},[117,6874,6809],{"class":1907},[117,6876,6812],{"class":130},[117,6878,683],{"class":134},[117,6880,6881],{"class":168},"\"chevron\"",[117,6883,6820],{"class":130},[117,6885,683],{"class":134},[117,6887,6825],{"class":168},[117,6889,2908],{"class":2907},[117,6891,1924],{"class":134},[117,6893,6894,6896,6898],{"class":119,"line":175},[117,6895,1939],{"class":134},[117,6897,2638],{"class":1907},[117,6899,1924],{"class":134},[117,6901,6902],{"class":119,"line":181},[117,6903,6904],{"class":187},"\u003C!-- ... and so on for each forwarded slot -->\n",[10,6906,4025,6907,6910,6911,6914,6915,6917],{},[29,6908,6909],{},"v-if=\"$slots.x\""," matters. Without it, a forwarded ",[29,6912,6913],{},"\u003Ctemplate #x>"," always exists from the parent's perspective, and ",[29,6916,6763],{},"'s own fallback rendering never gets a chance to run. The guard says \"only forward if the consumer actually provided this slot.\"",[10,6919,6920],{},"A realistic customization:",[108,6922,6924],{"className":1896,"code":6923,"language":1898,"meta":113,"style":113},"\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",[29,6925,6926,6965,6970,6975,6980,6985,6990,6995,7000,7004,7009,7014,7019,7024,7029,7034,7039,7043],{"__ignoreMap":113},[117,6927,6928,6930,6932,6934,6936,6938,6940,6942,6944,6946,6948,6950,6952,6954,6956,6958,6960,6963],{"class":119,"line":120},[117,6929,135],{"class":134},[117,6931,3648],{"class":1907},[117,6933,6022],{"class":130},[117,6935,512],{"class":134},[117,6937,5925],{"class":130},[117,6939,683],{"class":134},[117,6941,1916],{"class":168},[117,6943,5925],{"class":134},[117,6945,1916],{"class":168},[117,6947,6022],{"class":130},[117,6949,512],{"class":134},[117,6951,3734],{"class":130},[117,6953,683],{"class":134},[117,6955,1916],{"class":168},[117,6957,3734],{"class":134},[117,6959,1916],{"class":168},[117,6961,6962],{"class":130}," show-validation",[117,6964,1924],{"class":134},[117,6966,6967],{"class":119,"line":144},[117,6968,6969],{"class":134},"  \u003Ctemplate #suffix=\"{ validationState }\">\n",[117,6971,6972],{"class":119,"line":150},[117,6973,6974],{"class":134},"    \u003CSparkle v-if=\"validationState === 'valid'\" class=\"size-4 text-amber-400\" \u002F>\n",[117,6976,6977],{"class":119,"line":156},[117,6978,6979],{"class":134},"  \u003C\u002Ftemplate>\n",[117,6981,6982],{"class":119,"line":162},[117,6983,6984],{"class":134},"  \u003Ctemplate #error=\"{ message, reason }\">\n",[117,6986,6987],{"class":119,"line":175},[117,6988,6989],{"class":134},"    \u003Cp class=\"text-destructive text-xs\">\n",[117,6991,6992],{"class":119,"line":181},[117,6993,6994],{"class":134},"      \u003Ccode class=\"bg-muted rounded px-1 py-0.5\">{{ reason }}\u003C\u002Fcode> {{ message }}\n",[117,6996,6997],{"class":119,"line":193},[117,6998,6999],{"class":134},"    \u003C\u002Fp>\n",[117,7001,7002],{"class":119,"line":215},[117,7003,6979],{"class":134},[117,7005,7006],{"class":119,"line":221},[117,7007,7008],{"class":134},"  \u003Ctemplate #empty=\"{ query }\">\n",[117,7010,7011],{"class":119,"line":230},[117,7012,7013],{"class":134},"    \u003Cdiv class=\"px-3 py-6 text-center text-sm\">\n",[117,7015,7016],{"class":119,"line":242},[117,7017,7018],{"class":134},"      Nothing matched \"\u003Cstrong>{{ query }}\u003C\u002Fstrong>\".\n",[117,7020,7021],{"class":119,"line":285},[117,7022,7023],{"class":134},"      \u003Cbutton type=\"button\" class=\"underline\" @click=\"suggestCountry(query)\">\n",[117,7025,7026],{"class":119,"line":291},[117,7027,7028],{"class":134},"        Suggest a country\n",[117,7030,7031],{"class":119,"line":298},[117,7032,7033],{"class":134},"      \u003C\u002Fbutton>\n",[117,7035,7036],{"class":119,"line":304},[117,7037,7038],{"class":134},"    \u003C\u002Fdiv>\n",[117,7040,7041],{"class":119,"line":330},[117,7042,6979],{"class":134},[117,7044,7045,7047,7049],{"class":119,"line":335},[117,7046,1939],{"class":134},[117,7048,3648],{"class":1907},[117,7050,1924],{"class":134},[10,7052,7053,7054,7057],{},"A library component's job is to own ",[443,7055,7056],{},"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,7059,7060],{},"The same principle applies to the props that aren't visual:",[108,7062,7064],{"className":110,"code":7063,"language":112,"meta":113,"style":113},"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",[29,7065,7066,7098,7128,7138,7176],{"__ignoreMap":113},[117,7067,7068,7071,7073,7075,7078,7080,7082,7084,7087,7089,7091,7093,7095],{"class":119,"line":120},[117,7069,7070],{"class":134},"flagUrl",[117,7072,5692],{"class":123},[117,7074,1359],{"class":134},[117,7076,7077],{"class":202},"iso2",[117,7079,512],{"class":123},[117,7081,524],{"class":257},[117,7083,206],{"class":134},[117,7085,7086],{"class":202},"width",[117,7088,512],{"class":123},[117,7090,537],{"class":257},[117,7092,671],{"class":134},[117,7094,251],{"class":123},[117,7096,7097],{"class":134}," string;\n",[117,7099,7100,7103,7105,7107,7109,7111,7113,7115,7117,7119,7121,7123,7125],{"class":119,"line":144},[117,7101,7102],{"class":134},"searcher",[117,7104,5692],{"class":123},[117,7106,1359],{"class":134},[117,7108,6698],{"class":202},[117,7110,512],{"class":123},[117,7112,524],{"class":257},[117,7114,206],{"class":134},[117,7116,3734],{"class":202},[117,7118,512],{"class":123},[117,7120,6395],{"class":130},[117,7122,671],{"class":134},[117,7124,251],{"class":123},[117,7126,7127],{"class":134}," boolean;\n",[117,7129,7130,7133,7135],{"class":119,"line":150},[117,7131,7132],{"class":134},"countries",[117,7134,5692],{"class":123},[117,7136,7137],{"class":134}," CountryOption[];\n",[117,7139,7140,7142,7144,7146,7148,7150,7152,7154,7156,7158,7160,7163,7166,7168,7170,7172,7174],{"class":119,"line":156},[117,7141,5957],{"class":134},[117,7143,5692],{"class":123},[117,7145,1359],{"class":134},[117,7147,203],{"class":202},[117,7149,512],{"class":123},[117,7151,5117],{"class":130},[117,7153,671],{"class":134},[117,7155,251],{"class":123},[117,7157,5127],{"class":257},[117,7159,135],{"class":123},[117,7161,7162],{"class":134},"string ",[117,7164,7165],{"class":123},"|",[117,7167,1082],{"class":257},[117,7169,1556],{"class":123},[117,7171,1559],{"class":257},[117,7173,1553],{"class":123},[117,7175,527],{"class":134},[117,7177,7178,7181,7183,7186,7188,7191,7193],{"class":119,"line":162},[117,7179,7180],{"class":134},"errorMessages",[117,7182,5692],{"class":123},[117,7184,7185],{"class":134}," Record",[117,7187,135],{"class":123},[117,7189,7190],{"class":134},"PhoneValidationReason, string",[117,7192,1553],{"class":123},[117,7194,527],{"class":134},[10,7196,7197,7199,7200,7202,7203,7205,7206,7208],{},[29,7198,7070],{}," lets you swap flagcdn.com for your own CDN; useful if you have a CSP that won't allow third-party images. ",[29,7201,7102],{}," lets you replace substring-matching with prefix-matching, fuzzy-matching, Arabic-name matching, whatever your users actually want. ",[29,7204,7132],{}," 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. ",[29,7207,7180],{}," is i18n: pass an Arabic table and the validation reasons render in Arabic.",[10,7210,7211],{},"Every prop has a sensible default. The zero-config call still works.",[22,7213,7215],{"id":7214},"the-synchronous-dial-table-and-why-its-not-optional","The synchronous dial table, and why it's not optional",[10,7217,7218,7219,7222,7223,206,7226,7229,7230,7233,7234,7237,7238,5451,7240,7242],{},"The first time the component mounts in a fresh tab, the REST Countries fetch hasn't completed. Whatever indices ",[29,7220,7221],{},"usePhoneValidation"," builds (",[29,7224,7225],{},"byValue",[29,7227,7228],{},"byDialDigits",") are empty maps. If the consumer wrote ",[29,7231,7232],{},"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 ",[29,7235,7236],{},"[]",", the watcher would set ",[29,7239,3877],{},[29,7241,3795],{},", and Egypt wouldn't appear until the network call returned.",[10,7244,7245],{},"So the component bakes a synchronous fallback table:",[108,7247,7249],{"className":110,"code":7248,"language":112,"meta":113,"style":113},"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",[29,7250,7251,7277,7289,7301,7313,7325,7337,7349,7361,7373,7385,7397,7409,7421,7433,7445,7457,7469,7474],{"__ignoreMap":113},[117,7252,7253,7255,7258,7260,7262,7264,7266,7268,7270,7273,7275],{"class":119,"line":120},[117,7254,616],{"class":123},[117,7256,7257],{"class":257}," DIAL_TO_ISO2_FALLBACK",[117,7259,512],{"class":123},[117,7261,7185],{"class":130},[117,7263,135],{"class":134},[117,7265,3691],{"class":257},[117,7267,206],{"class":134},[117,7269,3691],{"class":257},[117,7271,7272],{"class":134},"> ",[117,7274,683],{"class":123},[117,7276,410],{"class":134},[117,7278,7279,7282,7284,7287],{"class":119,"line":144},[117,7280,7281],{"class":168},"  '1'",[117,7283,851],{"class":134},[117,7285,7286],{"class":168},"'US'",[117,7288,239],{"class":134},[117,7290,7291,7294,7296,7299],{"class":119,"line":150},[117,7292,7293],{"class":168},"  '7'",[117,7295,851],{"class":134},[117,7297,7298],{"class":168},"'RU'",[117,7300,239],{"class":134},[117,7302,7303,7306,7308,7311],{"class":119,"line":156},[117,7304,7305],{"class":168},"  '20'",[117,7307,851],{"class":134},[117,7309,7310],{"class":168},"'EG'",[117,7312,239],{"class":134},[117,7314,7315,7318,7320,7323],{"class":119,"line":162},[117,7316,7317],{"class":168},"  '27'",[117,7319,851],{"class":134},[117,7321,7322],{"class":168},"'ZA'",[117,7324,239],{"class":134},[117,7326,7327,7330,7332,7335],{"class":119,"line":175},[117,7328,7329],{"class":168},"  '33'",[117,7331,851],{"class":134},[117,7333,7334],{"class":168},"'FR'",[117,7336,239],{"class":134},[117,7338,7339,7342,7344,7347],{"class":119,"line":181},[117,7340,7341],{"class":168},"  '44'",[117,7343,851],{"class":134},[117,7345,7346],{"class":168},"'GB'",[117,7348,239],{"class":134},[117,7350,7351,7354,7356,7359],{"class":119,"line":193},[117,7352,7353],{"class":168},"  '49'",[117,7355,851],{"class":134},[117,7357,7358],{"class":168},"'DE'",[117,7360,239],{"class":134},[117,7362,7363,7366,7368,7371],{"class":119,"line":215},[117,7364,7365],{"class":168},"  '52'",[117,7367,851],{"class":134},[117,7369,7370],{"class":168},"'MX'",[117,7372,239],{"class":134},[117,7374,7375,7378,7380,7383],{"class":119,"line":221},[117,7376,7377],{"class":168},"  '55'",[117,7379,851],{"class":134},[117,7381,7382],{"class":168},"'BR'",[117,7384,239],{"class":134},[117,7386,7387,7390,7392,7395],{"class":119,"line":230},[117,7388,7389],{"class":168},"  '61'",[117,7391,851],{"class":134},[117,7393,7394],{"class":168},"'AU'",[117,7396,239],{"class":134},[117,7398,7399,7402,7404,7407],{"class":119,"line":242},[117,7400,7401],{"class":168},"  '81'",[117,7403,851],{"class":134},[117,7405,7406],{"class":168},"'JP'",[117,7408,239],{"class":134},[117,7410,7411,7414,7416,7419],{"class":119,"line":285},[117,7412,7413],{"class":168},"  '86'",[117,7415,851],{"class":134},[117,7417,7418],{"class":168},"'CN'",[117,7420,239],{"class":134},[117,7422,7423,7426,7428,7431],{"class":119,"line":291},[117,7424,7425],{"class":168},"  '91'",[117,7427,851],{"class":134},[117,7429,7430],{"class":168},"'IN'",[117,7432,239],{"class":134},[117,7434,7435,7438,7440,7443],{"class":119,"line":298},[117,7436,7437],{"class":168},"  '212'",[117,7439,851],{"class":134},[117,7441,7442],{"class":168},"'MA'",[117,7444,239],{"class":134},[117,7446,7447,7450,7452,7455],{"class":119,"line":304},[117,7448,7449],{"class":168},"  '966'",[117,7451,851],{"class":134},[117,7453,7454],{"class":168},"'SA'",[117,7456,239],{"class":134},[117,7458,7459,7462,7464,7467],{"class":119,"line":330},[117,7460,7461],{"class":168},"  '971'",[117,7463,851],{"class":134},[117,7465,7466],{"class":168},"'AE'",[117,7468,239],{"class":134},[117,7470,7471],{"class":119,"line":335},[117,7472,7473],{"class":187},"  \u002F\u002F ... ~50 entries\n",[117,7475,7476],{"class":119,"line":341},[117,7477,1506],{"class":134},[10,7479,7480,7481,7484],{},"And ",[29,7482,7483],{},"resolveCountryIdentifier"," checks the loaded index first, then this table:",[108,7486,7488],{"className":110,"code":7487,"language":112,"meta":113,"style":113},"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",[29,7489,7490,7521,7547,7564,7597,7628,7661,7678,7690,7705],{"__ignoreMap":113},[117,7491,7492,7494,7496,7498,7501,7503,7505,7507,7509,7511,7513,7515,7517,7519],{"class":119,"line":120},[117,7493,4193],{"class":123},[117,7495,3984],{"class":130},[117,7497,199],{"class":134},[117,7499,7500],{"class":202},"raw",[117,7502,512],{"class":123},[117,7504,524],{"class":257},[117,7506,1556],{"class":123},[117,7508,1559],{"class":257},[117,7510,1556],{"class":123},[117,7512,1082],{"class":257},[117,7514,271],{"class":134},[117,7516,512],{"class":123},[117,7518,524],{"class":257},[117,7520,410],{"class":134},[117,7522,7523,7525,7528,7530,7533,7536,7538,7540,7542,7545],{"class":119,"line":144},[117,7524,506],{"class":123},[117,7526,7527],{"class":257}," v",[117,7529,350],{"class":123},[117,7531,7532],{"class":130}," String",[117,7534,7535],{"class":134},"(raw ",[117,7537,4326],{"class":123},[117,7539,3937],{"class":168},[117,7541,3043],{"class":134},[117,7543,7544],{"class":130},"trim",[117,7546,5233],{"class":134},[117,7548,7549,7551,7553,7555,7558,7560,7562],{"class":119,"line":150},[117,7550,1417],{"class":123},[117,7552,1359],{"class":134},[117,7554,1589],{"class":123},[117,7556,7557],{"class":134},"v) ",[117,7559,1423],{"class":123},[117,7561,3937],{"class":168},[117,7563,527],{"class":134},[117,7565,7566,7568,7570,7572,7574,7577,7579,7581,7583,7585,7588,7590,7593,7595],{"class":119,"line":156},[117,7567,1417],{"class":123},[117,7569,1359],{"class":134},[117,7571,1204],{"class":168},[117,7573,1207],{"class":123},[117,7575,7576],{"class":257},"[A-Za-z]",[117,7578,5751],{"class":123},[117,7580,1204],{"class":168},[117,7582,17],{"class":134},[117,7584,5758],{"class":130},[117,7586,7587],{"class":134},"(v)) ",[117,7589,1423],{"class":123},[117,7591,7592],{"class":134}," v.",[117,7594,5275],{"class":130},[117,7596,5233],{"class":134},[117,7598,7599,7601,7604,7606,7608,7611,7613,7615,7617,7620,7622,7624,7626],{"class":119,"line":162},[117,7600,506],{"class":123},[117,7602,7603],{"class":257}," dial",[117,7605,350],{"class":123},[117,7607,7592],{"class":134},[117,7609,7610],{"class":130},"replace",[117,7612,199],{"class":134},[117,7614,1204],{"class":168},[117,7616,1207],{"class":123},[117,7618,7619],{"class":1214},"\\+",[117,7621,1204],{"class":168},[117,7623,206],{"class":134},[117,7625,3795],{"class":168},[117,7627,976],{"class":134},[117,7629,7630,7632,7634,7636,7638,7640,7643,7646,7648,7650,7652,7655,7657,7659],{"class":119,"line":175},[117,7631,1417],{"class":123},[117,7633,1359],{"class":134},[117,7635,1589],{"class":123},[117,7637,1204],{"class":168},[117,7639,1207],{"class":123},[117,7641,7642],{"class":257},"\\d",[117,7644,7645],{"class":123},"+$",[117,7647,1204],{"class":168},[117,7649,17],{"class":134},[117,7651,5758],{"class":130},[117,7653,7654],{"class":134},"(dial)) ",[117,7656,1423],{"class":123},[117,7658,3937],{"class":168},[117,7660,527],{"class":134},[117,7662,7663,7665,7667,7669,7671,7674,7676],{"class":119,"line":181},[117,7664,506],{"class":123},[117,7666,4899],{"class":257},[117,7668,350],{"class":123},[117,7670,4622],{"class":130},[117,7672,7673],{"class":134},"(dial)[",[117,7675,4606],{"class":257},[117,7677,1315],{"class":134},[117,7679,7680,7682,7685,7687],{"class":119,"line":193},[117,7681,1417],{"class":123},[117,7683,7684],{"class":134}," (match) ",[117,7686,1423],{"class":123},[117,7688,7689],{"class":134}," match.value;\n",[117,7691,7692,7694,7696,7699,7701,7703],{"class":119,"line":215},[117,7693,1498],{"class":123},[117,7695,7257],{"class":257},[117,7697,7698],{"class":134},"[dial] ",[117,7700,4326],{"class":123},[117,7702,3937],{"class":168},[117,7704,527],{"class":134},[117,7706,7707],{"class":119,"line":221},[117,7708,589],{"class":134},[10,7710,7711],{},"The reverse, going from an ISO2 back to a dial number for the v-model, does the same check:",[108,7713,7715],{"className":110,"code":7714,"language":112,"meta":113,"style":113},"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",[29,7716,7717,7744,7761,7775,7827,7843,7858,7882],{"__ignoreMap":113},[117,7718,7719,7721,7724,7726,7728,7730,7732,7734,7736,7738,7740,7742],{"class":119,"line":120},[117,7720,4193],{"class":123},[117,7722,7723],{"class":130}," dialNumberFor",[117,7725,199],{"class":134},[117,7727,7077],{"class":202},[117,7729,512],{"class":123},[117,7731,524],{"class":257},[117,7733,271],{"class":134},[117,7735,512],{"class":123},[117,7737,537],{"class":257},[117,7739,1556],{"class":123},[117,7741,1082],{"class":257},[117,7743,410],{"class":134},[117,7745,7746,7748,7750,7752,7755,7757,7759],{"class":119,"line":144},[117,7747,1417],{"class":123},[117,7749,1359],{"class":134},[117,7751,1589],{"class":123},[117,7753,7754],{"class":134},"iso2) ",[117,7756,1423],{"class":123},[117,7758,1082],{"class":257},[117,7760,527],{"class":134},[117,7762,7763,7765,7768,7770,7772],{"class":119,"line":150},[117,7764,506],{"class":123},[117,7766,7767],{"class":257}," fromIndex",[117,7769,350],{"class":123},[117,7771,4300],{"class":130},[117,7773,7774],{"class":134},"(iso2)?.raw_data?.dial_digits;\n",[117,7776,7777,7779,7782,7784,7787,7789,7792,7795,7797,7800,7802,7805,7808,7810,7813,7815,7818,7820,7823,7825],{"class":119,"line":156},[117,7778,506],{"class":123},[117,7780,7781],{"class":257}," digits",[117,7783,350],{"class":123},[117,7785,7786],{"class":134}," fromIndex ",[117,7788,4326],{"class":123},[117,7790,7791],{"class":134}," Object.",[117,7793,7794],{"class":130},"entries",[117,7796,199],{"class":134},[117,7798,7799],{"class":257},"DIAL_TO_ISO2_FALLBACK",[117,7801,3043],{"class":134},[117,7803,7804],{"class":130},"find",[117,7806,7807],{"class":134},"(([, ",[117,7809,6638],{"class":202},[117,7811,7812],{"class":134},"]) ",[117,7814,251],{"class":123},[117,7816,7817],{"class":134}," v ",[117,7819,1068],{"class":123},[117,7821,7822],{"class":134}," iso2)?.[",[117,7824,4606],{"class":257},[117,7826,1315],{"class":134},[117,7828,7829,7831,7833,7835,7837,7839,7841],{"class":119,"line":162},[117,7830,1417],{"class":123},[117,7832,1359],{"class":134},[117,7834,1589],{"class":123},[117,7836,4229],{"class":134},[117,7838,1423],{"class":123},[117,7840,1082],{"class":257},[117,7842,527],{"class":134},[117,7844,7845,7847,7850,7852,7855],{"class":119,"line":175},[117,7846,506],{"class":123},[117,7848,7849],{"class":257}," n",[117,7851,350],{"class":123},[117,7853,7854],{"class":130}," Number",[117,7856,7857],{"class":134},"(digits);\n",[117,7859,7860,7862,7865,7868,7871,7873,7876,7878,7880],{"class":119,"line":181},[117,7861,1498],{"class":123},[117,7863,7864],{"class":134}," Number.",[117,7866,7867],{"class":130},"isFinite",[117,7869,7870],{"class":134},"(n) ",[117,7872,1370],{"class":123},[117,7874,7875],{"class":134}," n ",[117,7877,512],{"class":123},[117,7879,1082],{"class":257},[117,7881,527],{"class":134},[117,7883,7884],{"class":119,"line":193},[117,7885,589],{"class":134},[10,7887,7888,7889,7892],{},"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 ",[29,7890,7891],{},"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,7894,7895],{},"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.",[22,7897,7899],{"id":7898},"recents-as-the-tiebreaker","Recents as the tiebreaker",[10,7901,7902,7903,7905],{},"The longest-prefix Tier 3 match isn't always unambiguous. ",[29,7904,3863],{}," 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,7907,7908,7909,512],{},"So Tier 3 reads from ",[29,7910,2977],{},[108,7912,7914],{"className":110,"code":7913,"language":112,"meta":113,"style":113},"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",[29,7915,7916,7930,7942,7980,8010,8022],{"__ignoreMap":113},[117,7917,7918,7920,7923,7925,7928],{"class":119,"line":120},[117,7919,616],{"class":123},[117,7921,7922],{"class":257}," recents",[117,7924,350],{"class":123},[117,7926,7927],{"class":130}," readRecents",[117,7929,5233],{"class":134},[117,7931,7932,7934,7937,7939],{"class":119,"line":144},[117,7933,616],{"class":123},[117,7935,7936],{"class":257}," recentHit",[117,7938,350],{"class":123},[117,7940,7941],{"class":134}," recents\n",[117,7943,7944,7947,7950,7952,7954,7956,7958,7961,7963,7965,7968,7970,7972,7975,7977],{"class":119,"line":150},[117,7945,7946],{"class":134},"  .",[117,7948,7949],{"class":130},"map",[117,7951,665],{"class":134},[117,7953,7077],{"class":202},[117,7955,671],{"class":134},[117,7957,251],{"class":123},[117,7959,7960],{"class":134}," group.",[117,7962,7804],{"class":130},[117,7964,665],{"class":134},[117,7966,7967],{"class":202},"c",[117,7969,671],{"class":134},[117,7971,251],{"class":123},[117,7973,7974],{"class":134}," c.value ",[117,7976,1068],{"class":123},[117,7978,7979],{"class":134}," iso2))\n",[117,7981,7982,7984,7986,7988,7990,7992,7994,7997,7999,8001,8004,8007],{"class":119,"line":156},[117,7983,7946],{"class":134},[117,7985,7804],{"class":130},[117,7987,665],{"class":134},[117,7989,7967],{"class":202},[117,7991,271],{"class":134},[117,7993,512],{"class":123},[117,7995,7996],{"class":202}," c",[117,7998,1031],{"class":123},[117,8000,6395],{"class":130},[117,8002,8003],{"class":123}," =>",[117,8005,8006],{"class":130}," Boolean",[117,8008,8009],{"class":134},"(c));\n",[117,8011,8012,8014,8017,8019],{"class":119,"line":162},[117,8013,5008],{"class":123},[117,8015,8016],{"class":134}," (recentHit) ",[117,8018,1423],{"class":123},[117,8020,8021],{"class":134}," { country: recentHit, nationalNumber };\n",[117,8023,8024,8026,8028,8030],{"class":119,"line":175},[117,8025,1423],{"class":123},[117,8027,4689],{"class":134},[117,8029,4606],{"class":257},[117,8031,4694],{"class":134},[10,8033,8034,8035,8038,8039,8041],{},"The picker writes to ",[29,8036,8037],{},"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 ",[29,8040,3751],{}," you type will resolve to Canada. If you've never used it, you get the alphabetical fallback: still wrong, but only once.",[10,8043,8044],{},"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.\"",[22,8046,8048],{"id":8047},"what-the-library-rewrite-was-really-about","What the library rewrite was really about",[10,8050,8051,8052,8055,8056,8059],{},"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 ",[29,8053,8054],{},"dropLeadingZeros"," setter, the seven ",[29,8057,8058],{},"PhoneValidationReason"," enum values, the LTR-wrapper-over-RTL-input bidi pattern. Those things weren't wrong; they were the foundation.",[10,8061,8062,8063,8066],{},"What the library rewrite added was everything that's specific to ",[443,8064,8065],{},"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,8068,8069],{},"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:",[108,8071,8073],{"className":1896,"code":8072,"language":1898,"meta":113,"style":113},"\u003CATellInput v-model:phone=\"phone\" v-model:country=\"country\" show-validation \u002F>\n",[29,8074,8075],{"__ignoreMap":113},[117,8076,8077,8079,8081,8083,8085,8087,8089,8091,8093,8095,8097,8099,8101,8103,8105,8107,8109,8111],{"class":119,"line":120},[117,8078,135],{"class":134},[117,8080,3648],{"class":1907},[117,8082,6022],{"class":130},[117,8084,512],{"class":134},[117,8086,5925],{"class":130},[117,8088,683],{"class":134},[117,8090,1916],{"class":168},[117,8092,5925],{"class":134},[117,8094,1916],{"class":168},[117,8096,6022],{"class":130},[117,8098,512],{"class":134},[117,8100,3734],{"class":130},[117,8102,683],{"class":134},[117,8104,1916],{"class":168},[117,8106,3734],{"class":134},[117,8108,1916],{"class":168},[117,8110,6962],{"class":130},[117,8112,2682],{"class":134},[10,8114,8115],{},"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,8117,8118,8119,8122,8123,8126],{},"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 ",[29,8120,8121],{},"restcountries.com"," hardcoded, every Saudi-and-Egypt fallback, every always-visible dropdown: those weren't wrong ",[443,8124,8125],{},"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.",[1812,8128,8129],{},"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":113,"searchDepth":144,"depth":144,"links":8131},[8132,8133,8134,8135,8136,8137,8138,8139],{"id":3665,"depth":144,"text":3666},{"id":4056,"depth":144,"text":4057},{"id":5076,"depth":144,"text":5077},{"id":5997,"depth":144,"text":5998},{"id":6143,"depth":144,"text":6144},{"id":7214,"depth":144,"text":7215},{"id":7898,"depth":144,"text":7899},{"id":8047,"depth":144,"text":8048},"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 لكل شيء.",[3625,3631,8144,8145,8146,8147,8148,8149,8150,1847],"phone input","libphonenumber-js","country detection","responsive popover","vaul-vue","defineModel","slots",{},"\u002Fwrites\u002Fatell-input-the-library-rewrite",{"title":3641,"description":8141},"writes\u002Fatell-input-the-library-rewrite","ملاحظات من إعادة كتابة ATellInput كمكتبة مُنشَرة","U2nw1NwLneEoQyPNZeM61sua6if230l-WTmnZI7oQVY",1793901269155]