@finesoft/front 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ const e={FLOW:`flow`,EXTERNAL_URL:`externalUrl`,COMPOUND:`compound`};function t(t){return t.kind===e.FLOW}function n(t){return t.kind===e.EXTERNAL_URL}function r(t){return t.kind===e.COMPOUND}function i(t,n){return{kind:e.FLOW,url:t,presentationContext:n}}function a(t){return{kind:e.EXTERNAL_URL,url:t}}var o=class{handlers=new Map;onAction(e,t){if(this.handlers.has(e)){console.warn(`[ActionDispatcher] kind="${e}" already registered, skipping`);return}this.handlers.set(e,t)}removeAction(e){return this.handlers.delete(e)}async perform(e,t=0){if(r(e)){if(t>=32)throw Error(`[ActionDispatcher] CompoundAction recursion depth exceeded (max 32)`);for(let n of e.actions)await this.perform(n,t+1);return}let n=this.handlers.get(e.kind);if(!n){console.warn(`[ActionDispatcher] No handler for kind="${e.kind}"`);return}await n(e)}},s=class{controllers=new Map;register(e){this.controllers.set(e.intentId,e)}async dispatch(e,t){let n=this.controllers.get(e.id);if(!n)throw Error(`[IntentDispatcher] No controller for "${e.id}". Registered: [${Array.from(this.controllers.keys()).join(`, `)}]`);return n.perform(e,t)}has(e){return this.controllers.has(e)}},c=class e{registrations=new Map;resolutionStack=new Set;parent;children=new Set;register(e,t,n=!0){return this.registrations.set(e,{factory:t,singleton:n}),this}resolve(e){let t=this.registrations.get(e);if(!t){if(this.parent)return this.parent.resolve(e);throw Error(`[Container] No registration for key: "${e}"`)}if(t.singleton){if(t.instance===void 0){if(this.resolutionStack.has(e))throw Error(`[Container] Circular dependency detected: ${[...this.resolutionStack,e].join(` → `)}`);this.resolutionStack.add(e);try{t.instance=t.factory()}finally{this.resolutionStack.delete(e)}}return t.instance}return t.factory()}has(e){return this.registrations.has(e)||(this.parent?.has(e)??!1)}unregister(e){return this.registrations.delete(e)}createScope(){let t=new e;return t.parent=this,this.children.add(t),t}dispose(){let e=Array.from(this.children);for(let t of e)t.dispose();this.children.clear();for(let e of this.registrations.values())e.instance=void 0;this.registrations.clear(),this.parent&&=(this.parent.children.delete(this),void 0)}};function l(e){if(!e)return{ok:!1,reason:`empty host`};let t=e.toLowerCase(),n=t.startsWith(`[`)&&t.endsWith(`]`)?t.slice(1,-1):t;if(n===`localhost`||n.endsWith(`.localhost`))return{ok:!1,reason:`loopback host forbidden`};if(n.includes(`:`))return m(n);let r=d(n);return r?p(r):{ok:!0}}function u(e){let t;try{t=new URL(e)}catch{return{ok:!1,reason:`invalid URL`}}return t.protocol!==`http:`&&t.protocol!==`https:`?{ok:!1,reason:`scheme "${t.protocol}" not allowed (only http/https)`}:l(t.hostname)}function d(e){if(e.length===0)return null;if(!e.includes(`.`)){let t=f(e);return t===null||t<0||t>4294967295?null:[t>>>24&255,t>>>16&255,t>>>8&255,t&255]}let t=e.split(`.`);if(t.length!==4)return null;let n=[];for(let e of t){let t=f(e);if(t===null||t<0||t>255)return null;n.push(t)}return n}function f(e){if(e.length===0)return null;if(e.startsWith(`0x`)||e.startsWith(`0X`)){let t=e.slice(2);return/^[0-9a-f]+$/i.test(t)?parseInt(t,16):null}return e.length>1&&e.startsWith(`0`)?/^[0-7]+$/.test(e)?parseInt(e,8):null:/^[0-9]+$/.test(e)?parseInt(e,10):null}function p(e){let[t,n]=e;return t===0?{ok:!1,reason:`unspecified IPv4 forbidden`}:t===127?{ok:!1,reason:`loopback IPv4 forbidden`}:t===10?{ok:!1,reason:`private IPv4 (10/8) forbidden`}:t===172&&n>=16&&n<=31?{ok:!1,reason:`private IPv4 (172.16/12) forbidden`}:t===192&&n===168?{ok:!1,reason:`private IPv4 (192.168/16) forbidden`}:t===169&&n===254?{ok:!1,reason:`link-local IPv4 forbidden`}:t===100&&n>=64&&n<=127?{ok:!1,reason:`CGNAT IPv4 forbidden`}:t>=224&&t<=239?{ok:!1,reason:`multicast IPv4 forbidden`}:t>=240?{ok:!1,reason:`reserved IPv4 forbidden`}:{ok:!0}}function m(e){let t=e.toLowerCase();if(t===`::`||t===`::0`)return{ok:!1,reason:`unspecified IPv6 forbidden`};if(t===`::1`)return{ok:!1,reason:`loopback IPv6 forbidden`};if(t.startsWith(`fe8`)||t.startsWith(`fe9`)||t.startsWith(`fea`)||t.startsWith(`feb`))return{ok:!1,reason:`link-local IPv6 forbidden`};if(t.startsWith(`fc`)||t.startsWith(`fd`))return{ok:!1,reason:`ULA IPv6 forbidden`};if(t.startsWith(`ff`))return{ok:!1,reason:`multicast IPv6 forbidden`};let n=h(t);if(n){let e=p(n);if(!e.ok)return{ok:!1,reason:`IPv4-in-IPv6 ${e.reason}`}}return{ok:!0}}function h(e){let t=e.match(/(?:^|:)((?:\d{1,3}\.){3}\d{1,3})$/);if(t){let e=d(t[1]);if(e)return e}let n=e.match(/(?:^|:)([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);if(n&&(e.startsWith(`::ffff:`)||e.includes(`:ffff:`))){let e=parseInt(n[1],16),t=parseInt(n[2],16);return[e>>>8&255,e&255,t>>>8&255,t&255]}return null}var g=class extends Error{constructor(e,t,n){super(`HTTP ${e}: ${t}`),this.status=e,this.statusText=t,this.body=n,this.name=`HttpError`}},_=class extends Error{constructor(e,t){super(`Refused to fetch ${e}: ${t}`),this.url=e,this.reason=t,this.name=`HostGuardError`}},v=class{baseUrl;defaultHeaders;fetchFn;requestInterceptors;responseInterceptors;allowInternalHosts;validateDns;constructor(e){this.baseUrl=e.baseUrl,this.defaultHeaders=e.defaultHeaders??{},this.fetchFn=e.fetch??globalThis.fetch.bind(globalThis),this.requestInterceptors=[...e.requestInterceptors??[]],this.responseInterceptors=[...e.responseInterceptors??[]],this.allowInternalHosts=e.allowInternalHosts??!1,this.validateDns=e.validateDns??!0}useRequestInterceptor(e){return this.requestInterceptors.push(e),this}useResponseInterceptor(e){return this.responseInterceptors.push(e),this}async get(e,t){return this.request(`GET`,e,{params:t})}async post(e,t,n){return this.request(`POST`,e,{body:t,params:n})}async put(e,t,n){return this.request(`PUT`,e,{body:t,params:n})}async del(e,t){return this.request(`DELETE`,e,{params:t})}async request(e,t,n){let r=this.buildUrl(t,n?.params);this.allowInternalHosts||await this.enforceHostGuard(r);let i={...this.defaultHeaders,...n?.headers},a={method:e,headers:i};n?.body!==void 0&&(Object.keys(i).some(e=>e.toLowerCase()===`content-type`)||(i[`Content-Type`]=`application/json`),a.body=JSON.stringify(n.body));for(let e of this.requestInterceptors)a=await e(r,a);let o=await this.fetchFn(r,a);for(let e of this.responseInterceptors)o=await e(o,r);if(!o.ok){let e=await o.text().catch(()=>void 0);throw new g(o.status,o.statusText,e)}try{return await o.json()}catch(e){throw e instanceof SyntaxError?new g(o.status,`Invalid JSON response`,await o.text().catch(()=>void 0)):e}}buildUrl(e,t){let n=this.baseUrl.endsWith(`/`)?this.baseUrl.slice(0,-1):this.baseUrl,r=e.startsWith(`/`)?e:`/${e}`,i=new URL(`${n}${r}`,`http://placeholder`);if(t)for(let[e,n]of Object.entries(t))i.searchParams.set(e,n);return this.baseUrl.startsWith(`http`)?i.toString():`${i.pathname}${i.search}`}async enforceHostGuard(e){let t;try{t=new URL(e)}catch{return}let n=u(e);if(!n.ok)throw new _(e,n.reason);if(this.validateDns&&t.hostname&&!y(t.hostname)){let n=await b(t.hostname);if(!n.ok)throw new _(e,n.reason)}}};function y(e){return e.includes(`:`)?!0:/^[0-9.]+$/.test(e)||/^0x[0-9a-f]+$/i.test(e)}async function b(e){let t;try{let e=await import(`node:dns/promises`);t=e.default??e}catch{return{ok:!0}}let n=await Promise.allSettled([t.resolve4(e),t.resolve6(e)]),r=[];for(let e of n)e.status===`fulfilled`&&r.push(...e.value);if(r.length===0)return{ok:!0};for(let e of r){let t=u(`http://${e.includes(`:`)?`[${e}]`:e}/`);if(!t.ok)return{ok:!1,reason:`host resolves to ${e} (${t.reason})`}}return{ok:!0}}function ee(e,t={}){let n=t.allowInternalHosts??!1,r=t.validateDns??!0;return async function(t,i){if(!n){let e=ne(t);if(e&&te(e)){let t=u(e);if(!t.ok)throw new _(e,t.reason);if(r){let t=new URL(e);if(t.hostname&&!x(t.hostname)){let n=await re(t.hostname);if(!n.ok)throw new _(e,n.reason)}}}}return e(t,i)}}function te(e){try{return new URL(e),!0}catch{return!1}}function ne(e){return typeof e==`string`?e:e instanceof URL?e.toString():typeof Request<`u`&&e instanceof Request?e.url:null}function x(e){return e.includes(`:`)?!0:/^[0-9.]+$/.test(e)||/^0x[0-9a-f]+$/i.test(e)}async function re(e){let t;try{let e=await import(`node:dns/promises`);t=e.default??e}catch{return{ok:!0}}let n;try{n=await t.lookup(e,{all:!0})}catch{return{ok:!0}}if(n.length===0)return{ok:!0};for(let{address:e}of n){let t=u(`http://${e.includes(`:`)?`[${e}]`:e}/`);if(!t.ok)return{ok:!1,reason:`host resolves to ${e} (${t.reason})`}}return{ok:!0}}const ie=new Set([`ar`,`arc`,`dv`,`fa`,`ha`,`he`,`khw`,`ks`,`ku`,`ps`,`ur`,`yi`]);function ae(e){let t=e.split(`-`)[0].toLowerCase();return ie.has(t)}function oe(e){return ae(e)?`rtl`:`ltr`}function se(e){return{lang:e,dir:oe(e)}}function ce(e,t){return{language:e,region:t,bcp47:t?`${e}-${t}`:e,dir:oe(e)}}function le(e){document.documentElement.lang=e.lang,document.documentElement.dir=e.dir}function ue(e,t){let n=e.split(`?`)[0].match(/^\/([^/]+)(\/.*)?$/);if(!n)return null;let r=n[1],i=t.find(e=>e.toLowerCase()===r.toLowerCase());return i?{locale:i,strippedUrl:n[2]||`/`}:null}async function de(e,t){if(!e||!t)return;let n=await fe();if(n)return n(e,t)}async function fe(){if(typeof globalThis.__FINESOFT_I18N_LOADER__==`function`)return globalThis.__FINESOFT_I18N_LOADER__;if(typeof __FINESOFT_I18N_LOADER_SPECIFIER__==`string`)try{let e=await import(__FINESOFT_I18N_LOADER_SPECIFIER__),t=typeof e==`function`?e:typeof e?.loadMessages==`function`?e.loadMessages:void 0;return t?(globalThis.__FINESOFT_I18N_LOADER__=t,t):void 0}catch{return}}async function pe(e){let{locale:t,loadMessages:n,context:r}=e;if(!(!t||!r))return n?n(t,r):de(t,r)}function me(e,t){let n=Object.entries(e);if(n.length===0)return;if(typeof n[0][1]==`string`)return e;let r=e[t];if(!r)return;let i={};for(let[e,t]of Object.entries(r)){if(typeof t==`string`){i[e]=t;continue}for(let[n,r]of Object.entries(t))i[`${e}.${n}`]=r}return i}function he(e,t){return t?e.replace(/\{(\w+)\}/g,(e,n)=>{let r=t[n];return r===void 0?`{${n}}`:String(r)}):e}function ge(e){return e===1?`one`:`other`}function _e(e,t){return`${e}.${t}`}var ve=class{locale;messages;pluralRule;fallback;constructor(e){this.locale=e.locale,this.messages=e.messages,this.pluralRule=e.pluralRule??ge,this.fallback=e.fallback??(e=>e)}t(e,t){let n=this.messages[e];return n===void 0?this.fallback(e):he(n,t)}plural(e,t,n){let r=_e(e,this.pluralRule(t)),i={count:t,...n};return this.t(r,i)}},ye=class{constructor(e){this.factories=e}loggerFor(e){return new be(this.factories.map(t=>t.loggerFor(e)))}},be=class{constructor(e){this.loggers=e}debug(...e){return this.callAll(`debug`,e)}info(...e){return this.callAll(`info`,e)}warn(...e){return this.callAll(`warn`,e)}error(...e){return this.callAll(`error`,e)}callAll(e,t){for(let n of this.loggers)n[e](...t);return``}},S=class{category;constructor(e){this.category=e}};const xe={"*":4,debug:4,info:3,warn:2,error:1,off:0,"":0};let C,w;function Se(){if(globalThis.localStorage===void 0)return{};let e;try{e=globalThis.localStorage.getItem(`onyxLog`)}catch{return{}}if(!e)return{};if(e===w&&C)return C;w=e;let t={},n=e.split(`,`);for(let e of n){let[n,r]=e.trim().split(`=`);if(!n||r===void 0)continue;let i=xe[r.toLowerCase()]??void 0;i!==void 0&&(n===`*`?t.defaultLevel=i:(t.named??={},t.named[n]=i))}return C=t,t}function T(e,t){let n=Se();if(n.defaultLevel===void 0&&!n.named)return!0;let r=xe[t]??4;return n.named?.[e]===void 0?n.defaultLevel===void 0?!0:r<=n.defaultLevel:r<=n.named[e]}function Ce(){C=void 0,w=void 0}var we=class extends S{debug(...e){return T(this.category,`debug`)&&console.debug(`[${this.category}]`,...e),``}info(...e){return T(this.category,`info`)&&console.info(`[${this.category}]`,...e),``}warn(...e){return T(this.category,`warn`)&&console.warn(`[${this.category}]`,...e),``}error(...e){return console.error(`[${this.category}]`,...e),``}},Te=class{loggerFor(e){return new we(e)}};const Ee={debug:0,info:1,warn:2,error:3};var De=class extends S{minPriority;report;constructor(e,t){super(e),this.minPriority=Ee[t.minLevel??`warn`],this.report=t.report}debug(...e){return this.maybeReport(`debug`,e),``}info(...e){return this.maybeReport(`info`,e),``}warn(...e){return this.maybeReport(`warn`,e),``}error(...e){return this.maybeReport(`error`,e),``}maybeReport(e,t){if(!(Ee[e]<this.minPriority))try{this.report(e,this.category,t)}catch(e){console.error(`[ReportingLogger] report callback threw:`,e)}}},Oe=class{options;constructor(e){this.options=e}loggerFor(e){return new De(e,this.options)}},ke=class{prefix;constructor(e=`Metrics`){this.prefix=e}record(e,t){console.info(`[${this.prefix}:${e}]`,t??``)}async flush(){}destroy(){}};function Ae(e){let t=e??(typeof navigator<`u`?navigator.userAgent:``),n=t.toLowerCase();return{os:je(n),browser:Me(n),engine:Ne(n),isMobile:/mobile|android|iphone|ipad|ipod/i.test(t),isTouch:typeof navigator<`u`&&`maxTouchPoints`in navigator?navigator.maxTouchPoints>0:!1}}function je(e){return/iphone|ipad|ipod/.test(e)?`ios`:/android/.test(e)?`android`:/macintosh|mac os x/.test(e)?`macos`:/windows/.test(e)?`windows`:/linux/.test(e)?`linux`:`unknown`}function Me(e){return/edg\//.test(e)?`edge`:/opr\/|opera/.test(e)?`opera`:/samsungbrowser/.test(e)?`samsung`:/chrome|crios/.test(e)&&!/edg\//.test(e)?`chrome`:/firefox|fxios/.test(e)?`firefox`:/safari/.test(e)&&!/chrome/.test(e)?`safari`:`unknown`}function Ne(e){return/applewebkit/.test(e)&&!/chrome/.test(e)?`webkit`:/applewebkit/.test(e)&&/chrome/.test(e)?`blink`:/gecko\//.test(e)?`gecko`:`unknown`}const E={LOGGER:`logger`,LOGGER_FACTORY:`loggerFactory`,NET:`net`,STORAGE:`storage`,FEATURE_FLAGS:`featureFlags`,METRICS:`metrics`,FETCH:`fetch`,SAFE_FETCH:`safeFetch`,EVENT_RECORDER:`eventRecorder`,LOCALE:`locale`,PLATFORM:`platform`,TRANSLATOR:`translator`};var Pe=class{store=new Map;get(e){return this.store.get(e)}set(e,t){this.store.set(e,t)}delete(e){this.store.delete(e)}},Fe=class{flags;providers=[];constructor(e={}){this.flags=e}addProvider(e){this.providers.push(e)}isEnabled(e){for(let t=this.providers.length-1;t>=0;t--)if(this.providers[t].isEnabled(e))return!0;return this.flags[e]===!0}getString(e){for(let t=this.providers.length-1;t>=0;t--){let n=this.providers[t].getString?.(e);if(n!==void 0)return n}let t=this.flags[e];return typeof t==`string`?t:void 0}getNumber(e){for(let t=this.providers.length-1;t>=0;t--){let n=this.providers[t].getNumber?.(e);if(n!==void 0)return n}let t=this.flags[e];return typeof t==`number`?t:void 0}},Ie=class{record(e,t){console.info(`[Metrics:${e}]`,t??``)}recordPageView(e,t){this.record(`PageView`,{page:e,...t})}recordEvent(e,t){this.record(`Event`,{name:e,...t})}};function Le(e,t={}){let{_resolvedMessages:n}=t,{fetch:r=globalThis.fetch?.bind(globalThis),featureFlags:i={},featureFlagsProviders:a=[],reportCallback:o,eventRecorder:s,locale:c,platform:l}=t,u=new Te,d=o?new ye([u,new Oe({report:o})]):u;e.register(E.LOGGER_FACTORY,()=>d),e.register(E.LOGGER,()=>d.loggerFor(`framework`)),e.register(E.NET,()=>({fetch:(e,t)=>r(e,t)})),e.register(E.STORAGE,()=>new Pe);let f=new Fe(i);for(let e of a)f.addProvider(e);if(e.register(E.FEATURE_FLAGS,()=>f),e.register(E.METRICS,()=>new Ie),e.register(E.EVENT_RECORDER,()=>s??new ke),c&&e.register(E.LOCALE,()=>se(c)),c&&n){let t=me(n,c);t&&e.register(E.TRANSLATOR,()=>new ve({locale:c,messages:t}))}e.register(E.PLATFORM,()=>l??Ae(typeof navigator<`u`?navigator.userAgent:void 0)),e.register(E.FETCH,()=>r);let p=t.safeFetch??{};e.register(E.SAFE_FETCH,()=>ee(r,p))}function D(e){return`container`in e?e.container:e}function Re(e){return{key:e,set(t,n){D(t).register(e,()=>n)},get(t){let n=D(t);return n.has(e)?n.resolve(e):void 0},clear(t){return D(t).unregister(e)}}}function O(e){return{"~standard":{version:1,vendor:`finesoft`,validate:e}}}async function ze(e,t){let n=await e[`~standard`].validate(t);return n.issues?{ok:!1,issues:n.issues}:{ok:!0,value:n.value}}function Be(e){return Object.assign(Object.create(null),e)}var Ve=class{routes=[];constructor(e){this.debug=e}add(e,t,n){let r=typeof n==`string`?{renderMode:n}:n??{},i=[],a=e.split(/(\/:[\w]+\??)/).map(t=>{let n=t.match(/^\/:(\w+)(\?)?$/);if(n){if(i.includes(n[1]))throw Error(`[Router] Duplicate parameter name ":${n[1]}" in pattern "${e}"`);return i.push(n[1]),n[2]?`(?:/([^/]+))?`:`/([^/]+)`}return t.replace(/[.+?^${}()|[\]\\]/g,`\\$&`)}).join(``);return this.routes.push({pattern:e,intentId:t,regex:RegExp(`^${a}/?$`),paramNames:i,renderMode:r.renderMode,beforeGuards:r.beforeGuards,afterGuards:r.afterGuards,paramCodecs:r.paramCodecs,queryCodecs:r.queryCodecs}),this}async resolve(e){let{path:t,queryParams:n}=this.parseUrl(e);for(let r of this.routes){let a=t.match(r.regex);if(!a)continue;let o=Be(),s=!0;for(let e=0;e<r.paramNames.length;e++){let t=r.paramNames[e],n=a[e+1],i=r.paramCodecs?.[t];if(i){let e=await ze(i,n);if(!e.ok){this.debug?.(`[Router] route "${r.pattern}" skipped: path param "${t}" failed validation: ${e.issues[0]?.message??`invalid`}`),s=!1;break}e.value!==void 0&&(o[t]=e.value)}else n&&(o[t]=n)}if(s){if(r.queryCodecs)for(let e of Object.keys(r.queryCodecs)){let t=await ze(r.queryCodecs[e],n[e]);if(!t.ok){this.debug?.(`[Router] route "${r.pattern}" skipped: query param "${e}" failed validation: ${t.issues[0]?.message??`invalid`}`),s=!1;break}t.value!==void 0&&(o[e]=t.value)}if(s)return Object.assign(o,Object.fromEntries(Object.entries(n).filter(([e])=>!(e in o)&&!r.queryCodecs?.[e]))),{intent:{id:r.intentId,params:o},action:i(e),renderMode:r.renderMode,beforeGuards:r.beforeGuards,afterGuards:r.afterGuards}}}return null}getRoutes(){return this.routes.map(e=>`${e.pattern} → ${e.intentId}`)}parseUrl(e){try{let t=new URL(e,`http://localhost`),n=Be(Object.fromEntries(t.searchParams));return{path:t.pathname,queryParams:n}}catch{return{path:e.split(`?`)[0].split(`#`)[0],queryParams:Be()}}}};const k=e=>({issues:[{message:e}]});function He(e={}){return O(t=>typeof t==`string`?e.minLength!==void 0&&t.length<e.minLength?k(`must be at least ${e.minLength} characters`):e.maxLength!==void 0&&t.length>e.maxLength?k(`must be at most ${e.maxLength} characters`):e.pattern&&!e.pattern.test(t)?k(`must match ${e.pattern}`):{value:t}:k(`expected a string`))}function Ue(e,t){return t.min!==void 0&&e<t.min?k(`must be >= ${t.min}`):t.max!==void 0&&e>t.max?k(`must be <= ${t.max}`):null}function We(e={}){return O(t=>{if(typeof t!=`string`||!/^-?\d+$/.test(t))return k(`"${String(t)}" is not an integer`);let n=Number(t);return Ue(n,e)??{value:n}})}function Ge(e={}){return O(t=>{if(typeof t!=`string`||!/^-?\d+(\.\d+)?$/.test(t))return k(`"${String(t)}" is not a number`);let n=Number(t);return Ue(n,e)??{value:n}})}const Ke=new Set([`true`,`1`]),qe=new Set([`false`,`0`]);function Je(){return O(e=>typeof e==`string`&&Ke.has(e)?{value:!0}:typeof e==`string`&&qe.has(e)?{value:!1}:k(`"${String(e)}" is not a boolean (true/false/1/0)`))}function Ye(e){return O(t=>typeof t==`string`&&e.includes(t)?{value:t}:k(`"${String(t)}" is not one of: ${e.join(`, `)}`))}const Xe=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;function Ze(){return O(e=>typeof e==`string`&&Xe.test(e)?{value:e}:k(`"${String(e)}" is not a valid UUID`))}function Qe(e){return O(t=>t===void 0?{value:void 0}:e[`~standard`].validate(t))}function $e(e,t){return O(n=>n===void 0?{value:t}:e[`~standard`].validate(n))}async function et(e,t){for(let n of e){let e=await n(t);if(e.kind!==`next`)return e}return{kind:`next`}}async function tt(e,t){for(let n of e){let e=await n(t);if(e.kind!==`next`)return e}return{kind:`next`}}const nt=new WeakMap;function A(e){return rt(e,new Set,0)}function rt(e,t,n){if(e==null)return String(e);if(typeof e!=`object`)return JSON.stringify(e);let r=nt.get(e);if(r!==void 0)return r;if(n>50)return`"[Max Depth]"`;if(t.has(e))return`"[Circular]"`;t.add(e);try{let r;if(Array.isArray(e))r=`[`+e.map(e=>rt(e,t,n+1)).join(`,`)+`]`;else{let i=Object.keys(e).sort(),a=[];for(let r of i){let i=e[r];i!==void 0&&a.push(JSON.stringify(r)+`:`+rt(i,t,n+1))}r=`{`+a.join(`,`)+`}`}return nt.set(e,r),r}finally{t.delete(e)}}var j=class e{intents;constructor(e){this.intents=e}static fromArray(t){let n=new Map;for(let e of t)if(e.intent&&e.data!==void 0){let t=A(e.intent);n.set(t,e.data)}return new e(n)}static empty(){return new e(new Map)}get(e){let t=A(e),n=this.intents.get(t);if(n!==void 0)return this.intents.delete(t),n}has(e){return this.intents.has(A(e))}get size(){return this.intents.size}},it=class e{container;intentDispatcher;actionDispatcher;router;prefetchedIntents;beforeGuards=[];afterGuards=[];_logger;constructor(e,t){this.container=e,this.intentDispatcher=new s,this.actionDispatcher=new o,this.router=new Ve(e=>this.getLogger().debug(e)),this.prefetchedIntents=t}static create(t={}){let n=new c;Le(n,t);let r=new e(n,t.prefetchedIntents??j.empty());return t.setupRoutes?.(r.router),r}getLogger(){return this._logger??=this.container.resolve(E.LOGGER)}async dispatch(e){let t=this.getLogger(),n=this.prefetchedIntents.get(e);return n===void 0?(t.debug(`[Framework] dispatch intent: ${e.id}`,e.params),this.intentDispatcher.dispatch(e,this.container)):(t.debug(`[Framework] re-using prefetched intent response for: ${e.id}`,e.params),n)}async perform(e){return this.getLogger().debug(`[Framework] perform action: ${e.kind}`),this.actionDispatcher.perform(e)}async routeUrl(e){return this.router.resolve(e)}didEnterPage(e){this.container.resolve(E.METRICS).recordPageView(e.pageType,{pageId:e.id,title:e.title})}getLocale(){return this.container.has(E.LOCALE)?this.container.resolve(E.LOCALE):void 0}getTranslator(){return this.container.has(E.TRANSLATOR)?this.container.resolve(E.TRANSLATOR):void 0}getPlatform(){return this.container.resolve(E.PLATFORM)}onAction(e,t){this.actionDispatcher.onAction(e,t)}registerIntent(e){this.intentDispatcher.register(e)}beforeLoad(e){this.beforeGuards.push(e)}afterLoad(e){this.afterGuards.push(e)}runBeforeLoad(e,t){return et(t?.length?[...this.beforeGuards,...t]:this.beforeGuards,e)}runAfterLoad(e,t){return tt(t?.length?[...this.afterGuards,...t]:this.afterGuards,e)}dispose(){this.container.dispose()}};const M=Symbol.for(`finesoft.publicFields`),at=[`id`,`pageType`,`title`,`description`,`url`];function ot(e,t){return t===!0?(Object.defineProperty(e,M,{value:!0,enumerable:!1,configurable:!0,writable:!1}),e):(Object.defineProperty(e,M,{value:Object.freeze([...t]),enumerable:!1,configurable:!0,writable:!1}),e)}function st(e){return typeof e==`object`&&!!e&&e[M]!==void 0}function ct(e){if(typeof e!=`object`||!e)return null;let t=e[M];return t===void 0?null:t===!0?!0:Array.isArray(t)?t:null}function lt(){try{let e=globalThis.process?.env?.NODE_ENV;return e===void 0?!0:e===`production`}catch{return!0}}function ut(e){if(e instanceof Error)return e.stack??`${e.name}: ${e.message}`;if(typeof e==`string`)return e;try{return JSON.stringify(e)}catch{return String(e)}}function dt(e){let{status:t,publicMessage:n,devError:r,isProduction:i=lt()}=e,a=!i&&r!==void 0;return{id:`error`,pageType:`error`,title:`Error ${t}`,description:a?`${n}\n\n[dev only]\n${ut(r)}`:n}}var ft=class{fallback(e,t){throw t}async perform(e,t){let n=e.params??{};try{return await this.execute(n,t)}catch(e){return this.fallback(n,e instanceof Error?e:Error(String(e)))}}};function pt(...e){return t=>e.reduce((e,t)=>t(e),t)}function mt(...e){return async t=>{let n=t;for(let t of e)n=await t(n);return n}}function ht(e){return t=>t.map(e)}const N={LEAF:`leaf`,STACK:`stack`,TABS:`tabs`,SPLIT:`split`},P={AUTOMATIC:`automatic`,ALL:`all`,DOUBLE_COLUMN:`doubleColumn`,DETAIL_ONLY:`detailOnly`};var F=class extends Error{constructor(e){super(e),this.name=`NavigationError`}};function I(e,t={}){return{kind:N.LEAF,intent:e,params:t}}function gt(e){let t=Array.isArray(e)?[...e]:[e];return{kind:N.STACK,entries:t}}function _t(e){let t=e.order?[...e.order]:Object.keys(e.branches);return{kind:N.TABS,active:e.active,order:t,branches:{...e.branches}}}function vt(e,t){let n=e.map(e=>({id:e.id,content:e.content}));return t===void 0?{kind:N.SPLIT,columns:n}:{kind:N.SPLIT,columns:n,visibility:t}}function L(e){return e.kind===N.LEAF}function yt(e){return e.kind===N.STACK}function bt(e){return e.kind===N.TABS}function xt(e){return e.kind===N.SPLIT}function R(e){let t=[],n=e;for(;;)switch(n.kind){case N.LEAF:return t;case N.STACK:{if(n.entries.length===0)return t;let e=n.entries.length-1;t.push({kind:`stack-entry`,index:e}),n=n.entries[e];break}case N.TABS:{let e=n.branches[n.active];if(e===void 0)return t;t.push({kind:`tab`,key:n.active}),n=e;break}case N.SPLIT:{let e=z(n);if(e===void 0)return t;t.push({kind:`column`,id:e.id}),n=e.content;break}}}function z(e){for(let t=e.columns.length-1;t>=0;t--)if(e.columns[t].content!==void 0)return e.columns[t]}function B(e,t){let n=e;for(let e of t){if(n===void 0)return;n=St(n,e)}return n}function St(e,t){switch(t.kind){case`stack-entry`:return e.kind===N.STACK?e.entries[t.index]:void 0;case`tab`:return e.kind===N.TABS?e.branches[t.key]:void 0;case`column`:return e.kind===N.SPLIT?e.columns.find(e=>e.id===t.id)?.content:void 0}}function Ct(e,t){let n=B(e,t);if(n===void 0)return;let r=[...t],i=n;for(;;)switch(i.kind){case N.STACK:return r;case N.LEAF:return;case N.TABS:{let e=i.branches[i.active];if(e===void 0)return;r.push({kind:`tab`,key:i.active}),i=e;break}case N.SPLIT:{let e=z(i);if(e===void 0)return;r.push({kind:`column`,id:e.id}),i=e.content;break}}}function wt(e){let t=[];return V(e,t),t}function V(e,t){switch(e.kind){case N.LEAF:t.push(e);return;case N.STACK:if(e.entries.length===0)return;V(e.entries[e.entries.length-1],t);return;case N.TABS:{let n=e.branches[e.active];n!==void 0&&V(n,t);return}case N.SPLIT:for(let n of Tt(e))n.content!==void 0&&V(n.content,t);return}}function Tt(e){let{columns:t,visibility:n}=e;if(t.length===0)return t;switch(n){case P.DETAIL_ONLY:return[t[t.length-1]];case P.DOUBLE_COLUMN:{let e=t.length-1;return e===0?[t[0]]:[t[0],t[e]]}default:return t}}function H(e,t,n){return t.length===0?n(e):U(e,t,0,n)}function U(e,t,n,r){if(n===t.length)return r(e);let i=t[n];switch(i.kind){case`stack-entry`:{if(e.kind!==N.STACK)throw new F(`路径无效:在 ${e.kind} 节点上期望 stack(stack-entry 步)`);if(i.index<0||i.index>=e.entries.length)throw new F(`路径无效:stack entry 索引越界 ${i.index}`);let a=e.entries.slice();return a[i.index]=U(a[i.index],t,n+1,r),{...e,entries:a}}case`tab`:{if(e.kind!==N.TABS)throw new F(`路径无效:在 ${e.kind} 节点上期望 tabs(tab 步)`);let a=e.branches[i.key];if(a===void 0)throw new F(`路径无效:tabs 分支 "${i.key}" 不存在`);return{...e,branches:{...e.branches,[i.key]:U(a,t,n+1,r)}}}case`column`:{if(e.kind!==N.SPLIT)throw new F(`路径无效:在 ${e.kind} 节点上期望 split(column 步)`);let a=e.columns.findIndex(e=>e.id===i.id);if(a===-1)throw new F(`路径无效:split 列 "${i.id}" 不存在`);let o=e.columns[a].content;if(o===void 0)throw new F(`路径无效:split 列 "${i.id}" 内容为空`);let s=e.columns.slice();return s[a]={...s[a],content:U(o,t,n+1,r)},{...e,columns:s}}}}function Et(e){let t=[],n,r=e;for(;;)switch(r.kind===N.STACK&&(n=[...t]),r.kind){case N.LEAF:return n;case N.STACK:{if(r.entries.length===0)return n;let e=r.entries.length-1;t.push({kind:`stack-entry`,index:e}),r=r.entries[e];break}case N.TABS:{let e=r.branches[r.active];if(e===void 0)return n;t.push({kind:`tab`,key:r.active}),r=e;break}case N.SPLIT:{let e=z(r);if(e===void 0)return n;t.push({kind:`column`,id:e.id}),r=e.content;break}}}function W(e,t){return t?Ct(e,t):Et(e)}function Dt(e,t,n){let r=W(e,n);if(r===void 0)throw new F(`push 失败:激活路径上没有可用的 stack`);return H(e,r,e=>{let n=e;return{...n,entries:[...n.entries,t]}})}function Ot(e,t=1,n){if(t<=0)return e;let r=W(e,n);if(r===void 0)throw new F(`pop 失败:激活路径上没有可用的 stack`);return H(e,r,e=>{let n=e,r=Math.max(1,n.entries.length-t);return r===n.entries.length?n:{...n,entries:n.entries.slice(0,r)}})}function kt(e,t){let n=W(e,t);if(n===void 0)throw new F(`popToRoot 失败:激活路径上没有可用的 stack`);return H(e,n,e=>{let t=e;return t.entries.length<=1?t:{...t,entries:t.entries.slice(0,1)}})}function At(e,t,n){let r=W(e,n);if(r===void 0)throw new F(`popTo 失败:激活路径上没有可用的 stack`);return H(e,r,e=>{let n=e;if(t<0||t>=n.entries.length)throw new F(`popTo 失败:index ${t} 越界(栈深 ${n.entries.length})`);return t===n.entries.length-1?n:{...n,entries:n.entries.slice(0,t+1)}})}function jt(e,t,n){let r=W(e,n);if(r===void 0)throw new F(`replaceTop 失败:激活路径上没有可用的 stack`);return H(e,r,e=>{let n=e;if(n.entries.length===0)throw new F(`replaceTop 失败:stack 为空`);let r=n.entries.slice();return r[r.length-1]=t,{...n,entries:r}})}function Mt(e,t,n){let r=n??Ft(e,N.TABS);if(r===void 0)throw new F(`selectTab 失败:激活路径上没有 tabs 节点`);return H(e,r,e=>{if(e.kind!==N.TABS)throw new F(`selectTab 失败:目标是 ${e.kind} 节点,不是 tabs`);let n=e;if(!(t in n.branches))throw new F(`selectTab 失败:未知分支 "${t}"`);return n.active===t?n:{...n,active:t}})}function Nt(e,t,n,r){let i=r??Ft(e,N.SPLIT);if(i===void 0)throw new F(`selectColumn 失败:激活路径上没有 split 节点`);return H(e,i,e=>{if(e.kind!==N.SPLIT)throw new F(`selectColumn 失败:目标是 ${e.kind} 节点,不是 split`);let r=e,i=r.columns.findIndex(e=>e.id===t);if(i===-1)throw new F(`selectColumn 失败:未知列 "${t}"`);let a=r.columns.map((e,t)=>t===i?{...e,content:n}:t>i?{...e,content:void 0}:e);return{...r,columns:a}})}function Pt(e,t,n){let r=n??Ft(e,N.SPLIT);if(r===void 0)throw new F(`setVisibility 失败:激活路径上没有 split 节点`);return H(e,r,e=>{if(e.kind!==N.SPLIT)throw new F(`setVisibility 失败:目标是 ${e.kind} 节点,不是 split`);return{...e,visibility:t}})}function Ft(e,t){let n=[],r=e;for(;;){if(r.kind===t)return n;switch(r.kind){case N.LEAF:return;case N.STACK:{if(r.entries.length===0)return;let e=r.entries.length-1;n.push({kind:`stack-entry`,index:e}),r=r.entries[e];break}case N.TABS:{let e=r.branches[r.active];if(e===void 0)return;n.push({kind:`tab`,key:r.active}),r=e;break}case N.SPLIT:{let e=z(r);if(e===void 0)return;n.push({kind:`column`,id:e.id}),r=e.content;break}}}}const It=new Set(Object.values(P));function G(e){switch(e.kind){case N.LEAF:return{kind:N.LEAF,intent:e.intent,params:e.params};case N.STACK:return{kind:N.STACK,entries:e.entries.map(G)};case N.TABS:{let t={};for(let n of Object.keys(e.branches))t[n]=G(e.branches[n]);return{kind:N.TABS,active:e.active,order:[...e.order],branches:t}}case N.SPLIT:{let t=e.columns.map(e=>({id:e.id,content:e.content===void 0?null:G(e.content)}));return e.visibility===void 0?{kind:N.SPLIT,columns:t}:{kind:N.SPLIT,columns:t,visibility:e.visibility}}}}function Lt(e){return A(G(e))}const Rt=new Set([N.LEAF,N.STACK,N.TABS,N.SPLIT]);function zt(e){return q(e,`$`)}function K(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function q(e,t){if(!K(e))throw new F(`反序列化失败:${t} 不是对象`);let n=e.kind;if(typeof n!=`string`||!Rt.has(n))throw new F(`反序列化失败:${t}.kind 非法(${String(n)})`);switch(n){case N.LEAF:return Bt(e,t);case N.STACK:return Vt(e,t);case N.TABS:return Ht(e,t);case N.SPLIT:return Ut(e,t)}}function Bt(e,t){if(typeof e.intent!=`string`)throw new F(`反序列化失败:${t}.intent 必须是字符串`);if(!K(e.params))throw new F(`反序列化失败:${t}.params 必须是对象`);return{kind:N.LEAF,intent:e.intent,params:{...e.params}}}function Vt(e,t){if(!Array.isArray(e.entries))throw new F(`反序列化失败:${t}.entries 必须是数组`);return{kind:N.STACK,entries:e.entries.map((e,n)=>q(e,`${t}.entries[${n}]`))}}function Ht(e,t){if(typeof e.active!=`string`)throw new F(`反序列化失败:${t}.active 必须是字符串`);if(!Array.isArray(e.order)||!e.order.every(e=>typeof e==`string`))throw new F(`反序列化失败:${t}.order 必须是字符串数组`);if(!K(e.branches))throw new F(`反序列化失败:${t}.branches 必须是对象`);let n={};for(let r of Object.keys(e.branches))n[r]=q(e.branches[r],`${t}.branches.${r}`);for(let r of e.order)if(!(r in n))throw new F(`反序列化失败:${t}.order 包含不在 branches 中的键 "${r}"`);if(!(e.active in n))throw new F(`反序列化失败:${t}.active "${e.active}" 不在 branches 中`);return{kind:N.TABS,active:e.active,order:[...e.order],branches:n}}function Ut(e,t){if(!Array.isArray(e.columns))throw new F(`反序列化失败:${t}.columns 必须是数组`);let n=e.columns.map((e,n)=>{let r=`${t}.columns[${n}]`;if(!K(e))throw new F(`反序列化失败:${r} 不是对象`);if(typeof e.id!=`string`)throw new F(`反序列化失败:${r}.id 必须是字符串`);let i=e.content===null||e.content===void 0?void 0:q(e.content,`${r}.content`);return{id:e.id,content:i}});if(e.visibility===void 0)return{kind:N.SPLIT,columns:n};if(typeof e.visibility!=`string`||!It.has(e.visibility))throw new F(`反序列化失败:${t}.visibility 非法值 ${JSON.stringify(e.visibility)}`);return{kind:N.SPLIT,columns:n,visibility:e.visibility}}const Wt=`__nav`;function J(e){try{let t=new URL(e,`http://localhost`);return{path:t.pathname,query:t.searchParams}}catch{let t=e.split(`?`)[0].split(`#`)[0],n=e.includes(`?`)?e.slice(e.indexOf(`?`)+1).split(`#`)[0]:``;return{path:t||`/`,query:new URLSearchParams(n)}}}function Gt(e,t){let n=t.toString();return n?`${e}?${n}`:e}function Kt(e){let t=[];for(let n of e.getRoutes()){let e=n.indexOf(` → `);if(e===-1)continue;let r=n.slice(0,e),i=n.slice(e+3);r&&i&&t.push({pattern:r,intentId:i})}return t}function qt(e){if(e!=null){if(typeof e==`string`)return e;if(typeof e==`number`)return Number.isFinite(e)?String(e):void 0;if(typeof e==`boolean`)return String(e)}}function Jt(e,t,n){for(let r of e){if(r.intentId!==t)continue;let e=Yt(r.pattern,n);if(e)return e}}function Yt(e,t){let n=new Set,r=!0,i=e.split(/(\/:[\w]+\??)/).map(e=>{let i=e.match(/^\/:(\w+)(\?)?$/);if(!i)return e;let a=i[1],o=i[2]===`?`,s=qt(t[a]);return s===void 0?(o||(r=!1),``):(n.add(a),`/${encodeURIComponent(s)}`)}).join(``);if(!r)return;let a=i===``?`/`:i,o=new URLSearchParams;for(let e of Object.keys(t).sort()){if(n.has(e))continue;let r=qt(t[e]);r!==void 0&&o.set(e,r)}return Gt(a,o)}function Xt(e,t,n){if(typeof e.reverse==`function`){let r=e.reverse(t,n);if(r!==void 0)return r}return Jt(Kt(e),t,n)}function Zt(e){return $t(A(G(e)))}function Qt(e){let t;try{t=en(e)}catch{throw new F(`导航参数解码失败:非法的 base64url`)}let n;try{n=JSON.parse(t)}catch{throw new F(`导航参数解码失败:非法的 JSON`)}return zt(n)}function $t(e){let t=an(e),n=``;for(let e of t)n+=String.fromCharCode(e);return tn(n).replace(/\+/g,`-`).replace(/\//g,`_`).replace(/=+$/,``)}function en(e){let t=nn(e.replace(/-/g,`+`).replace(/_/g,`/`)),n=new Uint8Array(t.length);for(let e=0;e<t.length;e++)n[e]=t.charCodeAt(e);return on(n)}function tn(e){return typeof btoa==`function`?btoa(e):rn(e,`binary`).toString(`base64`)}function nn(e){return typeof atob==`function`?atob(e):rn(e,`base64`).toString(`binary`)}function rn(e,t){let n=globalThis.Buffer;if(!n)throw new F(`导航参数编解码失败:当前运行时缺少 base64 能力`);return n.from(e,t)}function an(e){if(typeof TextEncoder<`u`)return new TextEncoder().encode(e);let t=[];for(let n of e){let e=n.codePointAt(0);e<128?t.push(e):e<2048?t.push(192|e>>6,128|e&63):e<65536?t.push(224|e>>12,128|e>>6&63,128|e&63):t.push(240|e>>18,128|e>>12&63,128|e>>6&63,128|e&63)}return Uint8Array.from(t)}function on(e){if(typeof TextDecoder<`u`)return new TextDecoder().decode(e);let t=``;for(let n=0;n<e.length;){let r=e[n++];r<128?t+=String.fromCodePoint(r):r<224?t+=String.fromCodePoint((r&31)<<6|e[n++]&63):r<240?t+=String.fromCodePoint((r&15)<<12|(e[n++]&63)<<6|e[n++]&63):t+=String.fromCodePoint((r&7)<<18|(e[n++]&63)<<12|(e[n++]&63)<<6|e[n++]&63)}return t}function sn(e){if(L(e))return{intent:e.intent,params:e.params};let t=B(e,R(e));if(t!==void 0&&L(t))return{intent:t.intent,params:t.params}}function cn(){return{encode(e,t){let n=sn(e);return n===void 0?`/`:Xt(t,n.intent,n.params)??`/`},decode(e){let{query:t}=J(e),n=t.get(Wt);if(!(n===null||n===``))return Qt(n)}}}function ln(e={}){let t=e.param??`__nav`;return{encode(e,n){let r=sn(e),{path:i,query:a}=J(r?Xt(n,r.intent,r.params)??`/`:`/`);return a.set(t,Zt(e)),Gt(i,a)},decode(e){let{query:n}=J(e),r=n.get(t);if(!(r===null||r===``))return Qt(r)}}}const Y={PUSH:`push`,POP:`pop`,POP_TO_ROOT:`popToRoot`,POP_TO:`popTo`,REPLACE_TOP:`replaceTop`,SELECT_TAB:`selectTab`,SELECT_COLUMN:`selectColumn`,SET_VISIBILITY:`setVisibility`,HYDRATE:`hydrate`};function un(e,t){return`${e}${A(t)}`}function dn(e,t){return{id:`error-${e}`,pageType:`error`,title:t,description:t}}function fn(e){let{intentDispatcher:t,router:n,createContext:r,beforeLoad:i=[],afterLoad:a=[],prefetched:o,getErrorPage:s=dn,onRedirect:c}=e,l=e.isServer??typeof window>`u`,u=e.initial,d={tree:u,destinations:[]},f=!1,p=new Set,m=Promise.resolve(d);function h(e){let t=m.then(e,e);return m=t.then(e=>e,()=>d),t}async function g(e,t){let n=new Map;for(let e of t.destinations)n.set(un(e.intent,e.params),e);let r=wt(e),i=pn(e),a=i?un(i.intent,i.params):void 0,o=[];for(let e of r){let t=un(e.intent,e.params),r=a!==void 0&&t===a,i=n.get(t);r?o.push(await _(e,i?.page)):i===void 0?o.push(await v(e)):o.push(i)}return{tree:e,destinations:o}}async function _(e,t){let n=e.intent,o=e.params,l=r({intent:n,params:o}),u=l.navigation??b(n,o,l),d=await et(i,u);if(d.kind===`redirect`)return c?.({url:d.url,status:d.status}),{intent:n,params:o,page:t??s(d.status,`Redirecting`),status:d.status};if(d.kind===`deny`)return{intent:n,params:o,page:s(d.status,d.message),status:d.status};if(d.kind===`rewrite`){let e=await ee(d.url);e&&(n=e.intent,o=e.params,l=r({intent:n,params:o}),u=l.navigation??b(n,o,l),t=void 0)}let f=await y(n,o,l,t);if(f.status!==void 0)return{intent:n,params:o,page:f.page,status:f.status};let p=await tt(a,{...u,page:f.page});return p.kind===`redirect`?(c?.({url:p.url,status:p.status}),{intent:n,params:o,page:f.page,status:p.status}):p.kind===`deny`?{intent:n,params:o,page:f.page,status:p.status}:{intent:n,params:o,page:f.page}}async function v(e){let t=r({intent:e.intent,params:e.params}),n=await y(e.intent,e.params,t,void 0);return{intent:e.intent,params:e.params,page:n.page,...n.status===void 0?{}:{status:n.status}}}async function y(e,n,r,i){if(i!==void 0)return{page:i};let a={id:e,params:n},c=o?.get(a);if(c!==void 0)return{page:c};try{return{page:await t.dispatch(a,r.container)}}catch{return{page:s(500,`Internal error`),status:500}}}function b(e,t,n){let r=n.url??`/`,i;try{i=new URL(r,`http://localhost`).pathname}catch{i=r.split(`?`)[0].split(`#`)[0]}return{url:r,path:i,params:t,intent:{id:e,params:t},isServer:l,container:n.container,getCookie:()=>void 0,getHeader:()=>void 0}}async function ee(e){let t=await n.resolve(e);if(t)return I(t.intent.id,t.intent.params??{})}function te(e){u=e.tree,d=e,f=!0;for(let e of p)e(d);return d}function ne(e){switch(e.kind){case Y.PUSH:return Dt(u,I(e.intent,e.params),e.target);case Y.POP:return Ot(u,e.count,e.target);case Y.POP_TO_ROOT:return kt(u,e.target);case Y.POP_TO:return At(u,e.index,e.target);case Y.REPLACE_TOP:return jt(u,I(e.intent,e.params),e.target);case Y.SELECT_TAB:return Mt(u,e.key,e.target);case Y.SELECT_COLUMN:return Nt(u,e.columnId,e.intent===void 0?void 0:I(e.intent,e.params),e.target);case Y.SET_VISIBILITY:return Pt(u,e.visibility,e.target);case Y.HYDRATE:return e.tree}}function x(e){return h(async()=>te(await g(ne(e),d)))}return{getTree(){return u},getSnapshot(){return d},apply:x,push(e,t,n){return x({kind:Y.PUSH,intent:e,params:t,target:n?.target})},pop(e){return x({kind:Y.POP,count:e})},popToRoot(){return x({kind:Y.POP_TO_ROOT})},replaceTop(e,t){return x({kind:Y.REPLACE_TOP,intent:e,params:t})},selectTab(e,t){return x({kind:Y.SELECT_TAB,key:e,target:t})},selectColumn(e,t,n,r){return x({kind:Y.SELECT_COLUMN,columnId:e,intent:t,params:n,target:r})},setVisibility(e,t){return x({kind:Y.SET_VISIBILITY,visibility:e,target:t})},hydrate(e){return x({kind:Y.HYDRATE,tree:e})},subscribe(e){return p.add(e),()=>{p.delete(e)}},resolve(){return h(async()=>te(await g(u,f?d:{tree:u,destinations:[]})))}}}function pn(e){let t=B(e,R(e));if(t!==void 0)return t.kind===`leaf`?t:void 0}function mn(e){return typeof e!=`object`||!e?!1:L(e)||yt(e)||bt(e)||xt(e)}function hn(e,t){if(mn(e))return e;let n=e(t);return n===void 0?{kind:`leaf`,intent:`@finesoft/navigation-root`,params:{}}:n}function gn(e){return mn(e)?()=>e:e}function _n(){return typeof window<`u`&&window.location?window.location.pathname+window.location.search:`/`}function vn(e){let t=e.codec??cn(),{initial:n,beforeLoad:r,afterLoad:i,getErrorPage:a}=e;return{initial:n,codec:t,beforeLoad:r,afterLoad:i,getErrorPage:a,toBrowserConfig(e){return{initial:hn(n,e??_n()),codec:t,beforeLoad:r,afterLoad:i,getErrorPage:a}},toSSRDefinition(){return{codec:t,initial:gn(n),beforeLoad:r,afterLoad:i}}}}function yn(e,t){return{path:e,...t}}function bn(e,t,n){let r=new Set;for(let i of t){i.controller&&!r.has(i.intentId)&&(e.registerIntent(i.controller),r.add(i.intentId));let t={renderMode:i.renderMode,beforeGuards:i.beforeLoad,afterGuards:i.afterLoad,paramCodecs:i.params,queryCodecs:i.query};if(e.router.add(i.path,i.intentId,t),n?.locales?.length){let n=i.path===`/`?`/:locale`:`/:locale${i.path}`;e.router.add(n,i.intentId,t)}}}var xn=class{map=new Map;capacity;constructor(e){if(e<1)throw Error(`[LruMap] capacity must be >= 1, got ${e}`);this.capacity=e}get(e){if(!this.map.has(e))return;let t=this.map.get(e);return this.map.delete(e),this.map.set(e,t),t}set(e,t){if(this.map.has(e))this.map.delete(e);else if(this.map.size>=this.capacity){let e=this.map.keys().next().value;e!==void 0&&this.map.delete(e)}this.map.set(e,t)}has(e){return this.map.has(e)}delete(e){return this.map.delete(e)}get size(){return this.map.size}clear(){this.map.clear()}};function Sn(e){return e!=null}function Cn(e){return e==null}function wn(){return typeof window>`u`?`browser`:document.referrer.startsWith(`android-app://`)?`twa`:window.matchMedia(`(display-mode: standalone)`).matches||`standalone`in window.navigator&&window.navigator.standalone===!0?`standalone`:`browser`}function Tn(e){return e.replace(/^https?:\/\//,``)}function En(e){try{let t=new URL(e);return t.pathname+t.search+t.hash}catch{return e}}function Dn(e){return e.split(`?`)[0]}function On(e){return e.split(`?`)[0].split(`#`)[0]}function kn(e,t){if(!t)return e;let n=new URLSearchParams;for(let[e,r]of Object.entries(t))r!==void 0&&n.set(e,r);let r=n.toString();return r?`${e}?${r}`:e}function X(){return typeof crypto<`u`&&crypto.randomUUID?crypto.randomUUID():`xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`.replace(/[xy]/g,e=>{let t=Math.random()*16|0;return(e===`x`?t:t&3|8).toString(16)})}function An(e){let t=new Map;if(!e)return t;for(let n of e.split(`;`)){let e=n.indexOf(`=`);if(e===-1)continue;let r=n.slice(0,e).trim(),i=n.slice(e+1).trim();r&&t.set(r,i)}return t}function jn(e){let{url:t,intent:n,container:r,request:i}=e,a=new URL(t,`http://localhost`),o=An(i?.headers.get(`cookie`)??``);return{url:t,path:a.pathname,params:n.params??{},intent:n,isServer:!0,container:r,getCookie:e=>o.get(e),getHeader:e=>i?.headers.get(e)??void 0}}function Z(e){let{url:t,intent:n,container:r}=e;return{url:t,path:new URL(t,window.location.origin).pathname,params:n.params??{},intent:n,isServer:!1,container:r,getCookie:e=>An(document.cookie).get(e),getHeader:()=>void 0}}function Mn(){return{kind:`next`}}function Nn(e,t=302){return{kind:`redirect`,url:e,status:t}}function Pn(e){return{kind:`rewrite`,url:e}}function Fn(e=403,t=`Forbidden`){return{kind:`deny`,status:e,message:t}}var In=class{recorders;constructor(e){this.recorders=e}record(e,t){for(let n of this.recorders)n.record(e,t)}async flush(){let e=[];for(let t of this.recorders)t.flush&&e.push(t.flush());await Promise.all(e)}destroy(){for(let e of this.recorders)e.destroy?.()}},Ln=class{observer;tracked=new Map;captured=[];minDuration;constructor(e={}){this.minDuration=e.minVisibleDuration??1e3,this.observer=new IntersectionObserver(e=>{let t=Date.now();for(let n of e){let e=this.tracked.get(n.target);e&&(n.isIntersecting?e.visibleSince===null&&(e.visibleSince=t):e.visibleSince!==null&&(t-e.visibleSince>=this.minDuration&&this.captured.push({id:e.id,timestamp:e.visibleSince,metadata:e.metadata}),e.visibleSince=null))}},{threshold:e.threshold??.5})}observe(e,t,n){this.tracked.set(e,{id:t,metadata:n,visibleSince:null}),this.observer.observe(e)}unobserve(e){this.observer.unobserve(e),this.tracked.delete(e)}consume(){let e=Date.now();for(let[,t]of this.tracked)t.visibleSince!==null&&e-t.visibleSince>=this.minDuration&&(this.captured.push({id:t.id,timestamp:t.visibleSince,metadata:t.metadata}),t.visibleSince=e);return this.captured.splice(0)}destroy(){this.observer.disconnect(),this.tracked.clear(),this.captured.length=0}},Rn=class{record(){}async flush(){}destroy(){}},zn=class{constructor(e,t){this.inner=e,this.providers=t}record(e,t){let n={};for(let e of this.providers)Object.assign(n,e.getFields());t&&Object.assign(n,t),this.inner.record(e,n)}async flush(){return this.inner.flush?.()}destroy(){this.inner.destroy?.()}};function Bn(t){let{framework:n,log:r}=t;n.onAction(e.EXTERNAL_URL,e=>{r.debug(`ExternalUrlAction → ${e.url}`),window.open(e.url,`_blank`,`noopener,noreferrer`)})}const Vn=5e3;let Q=null;function $(){Q?.()}function Hn(e,t,n){$();let r=Math.max(0,n),i=Date.now(),a=!1,o=null,s=null,c=null,l=null;Q=p,f(),document.addEventListener(`load`,u,!0),s=setInterval(u,100),c=setTimeout(u,Vn),u();function u(){a||o!==null||(o=requestAnimationFrame(()=>{o=null,d()}))}function d(){if(a)return;let n=Date.now()-i,o=t();if(!o){n>=Vn&&(e.warn(`tryScroll: timed out waiting for the scrollable element`,{target:r,elapsedMs:n}),p());return}o.scrollTop=r;let s=o.scrollTop;if(s>=r-2){e.info(`scroll restored`,{target:r,actual:s,elapsedMs:n}),p();return}n>=Vn&&(e.warn(`tryScroll: timed out before reaching the target`,{target:r,actual:s,elapsedMs:n,scrollHeight:o.scrollHeight,clientHeight:o.clientHeight}),p())}function f(){if(typeof MutationObserver>`u`)return;let e=document.body??document.documentElement;e&&(l=new MutationObserver(()=>{u()}),l.observe(e,{childList:!0,subtree:!0}))}function p(){a||(a=!0,Q===p&&(Q=null),o!==null&&(cancelAnimationFrame(o),o=null),s!==null&&(clearInterval(s),s=null),c!==null&&(clearTimeout(c),c=null),l?.disconnect(),l=null,document.removeEventListener(`load`,u,!0))}}var Un=class{entries;log;getScrollablePageElement;persistInHistoryState;currentStateId;constructor(e,t,n=10){this.entries=new xn(n),this.log=e,this.getScrollablePageElement=t.getScrollablePageElement,this.persistInHistoryState=t.persistInHistoryState??!1}historyState(e,t){return this.persistInHistoryState?{id:e,state:t}:{id:e}}replaceState(e,t){$();let n=X();window.history.replaceState(this.historyState(n,e),``,t),this.currentStateId=n,this.entries.set(n,{state:e,scrollY:0}),this.scrollTop=0,this.log.info(`replaceState`,e,t,n)}pushState(e,t){$();let n=X();window.history.pushState(this.historyState(n,e),``,t),this.currentStateId=n,this.entries.set(n,{state:e,scrollY:0}),this.scrollTop=0,this.log.info(`pushState`,e,t,n)}beforeTransition(){$();let{state:e}=window.history;if(!e)return;let t=this.entries.get(e.id);if(!t){this.log.info(`current history state evicted from LRU, not saving scroll position`);return}let{scrollTop:n}=this;this.entries.set(e.id,{...t,scrollY:n}),this.log.info(`saving scroll position`,n)}onPopState(e){window.addEventListener(`popstate`,t=>{$(),this.currentStateId=t.state?.id,this.currentStateId||this.log.warn(`encountered a null event.state.id in onPopState event:`,window.location.href),this.log.info(`popstate`,this.entries,this.currentStateId);let n=this.currentStateId?this.entries.get(this.currentStateId):void 0,r=this.persistInHistoryState?t.state?.state:void 0,i=n?.state??r;if(Promise.resolve(e(window.location.href,i)).catch(e=>{this.log.error(`onPopState listener error:`,e)}),!n)return;let{scrollY:a}=n;this.log.info(`restoring scroll to`,a),Hn(this.log,()=>this.getScrollablePageElement(),a)})}pushUrl(e){$();let t=X();window.history.pushState({id:t},``,e),this.currentStateId=t,this.scrollTop=0,this.log.info(`pushUrl (no state)`,e,t)}replaceUrl(e){$();let t=X();window.history.replaceState({id:t},``,e),this.currentStateId=t,this.scrollTop=0,this.log.info(`replaceUrl (no state)`,e,t)}updateState(e){if(!this.currentStateId){this.log.warn(`failed: encountered a null currentStateId inside updateState`);return}let t=this.entries.get(this.currentStateId),n=e(t?.state);this.log.info(`updateState`,n,this.currentStateId),this.entries.set(this.currentStateId,{scrollY:t?.scrollY??0,state:n})}get scrollTop(){return this.getScrollablePageElement()?.scrollTop||0}set scrollTop(e){let t=this.getScrollablePageElement();t&&(t.scrollTop=e)}};function Wn(t){let{framework:n,log:r,callbacks:i,updateApp:a}=t,o=t.manageHistory??!0,s=!0,c=0,l=o?new Un(r,{getScrollablePageElement:t.getScrollablePageElement??(()=>document.getElementById(`scrollable-page-override`)||document.getElementById(`scrollable-page`)||document.documentElement)}):void 0;async function u(e,t,o){if(t>=5){r.error(`Navigation redirect loop detected (5 redirects), stopping at: ${e}`);return}let f=s||e===window.location.pathname+window.location.search,p=await n.routeUrl(e);if(!p){r.warn(`FlowAction: no route for ${e}`);return}let m=Z({url:e,intent:p.intent,container:n.container}),h=await n.runBeforeLoad(m,p.beforeGuards);if(h.kind===`redirect`){r.debug(`beforeLoad → redirect to ${h.url}`),await u(h.url,t+1,o);return}if(h.kind===`deny`){r.warn(`beforeLoad → denied (${h.status}): ${h.message}`);return}if(h.kind===`rewrite`){r.debug(`beforeLoad → rewrite to ${h.url}`),await u(h.url,t+1,o);return}let g=n.dispatch(p.intent);if(await Promise.race([g,new Promise(e=>setTimeout(e,500))]).catch(()=>{}),o!==c){r.info(`FlowAction superseded by newer navigation`,e);return}l?.beforeTransition(),a({page:g.then(async a=>{if(o!==c)return r.info(`FlowAction commit superseded`,e),a;let s={...m,page:a},h=await n.runAfterLoad(s,p.afterGuards);if(h.kind===`redirect`)return r.debug(`afterLoad → redirect to ${h.url}`),u(h.url,t+1,o),a;let g=e;return h.kind===`rewrite`&&(g=h.url,r.debug(`afterLoad → rewrite URL to ${g}`)),h.kind===`deny`?(r.warn(`afterLoad → denied (${h.status})`),a):(f?l?.replaceState({page:a},g):l?.pushState({page:a},g),i.onNavigate(new URL(g,window.location.origin).pathname),d(a),a)},t=>{if(o===c){let t=e;f?l?.replaceUrl(t):l?.pushUrl(t),i.onNavigate(new URL(t,window.location.origin).pathname)}throw t}),isFirstPage:s}),s=!1}n.onAction(e.FLOW,async e=>{let t=e,a=t.url;if(r.debug(`FlowAction → ${a}`),t.presentationContext===`modal`){let e=await n.routeUrl(a);if(e){let t=await n.dispatch(e.intent);i.onModal(t)}return}await u(a,0,++c)}),l?.onPopState(async(e,t)=>{if(r.debug(`popstate → ${e}, cached=${!!t}`),i.onNavigate(new URL(e).pathname),t){let{page:e}=t;d(e),a({page:e,isFirstPage:s});return}let o=new URL(e),l=await n.routeUrl(o.pathname+o.search);if(!l){r.error(`received popstate without data, but URL was unroutable:`,e),d(null),a({page:Promise.reject(Error(`404`)),isFirstPage:s});return}let f=Z({url:o.pathname+o.search,intent:l.intent,container:n.container}),p=await n.runBeforeLoad(f,l.beforeGuards);if(p.kind===`redirect`){r.debug(`popstate beforeLoad → redirect to ${p.url}`);let e=++c;await u(p.url,0,e);return}if(p.kind===`deny`||p.kind===`rewrite`){if(p.kind===`deny`)r.warn(`popstate beforeLoad → denied`);else{let e=++c;await u(p.url,0,e)}return}let m=n.dispatch(l.intent);await Promise.race([m,new Promise(e=>setTimeout(e,500))]).catch(()=>{}),a({page:m.then(async e=>{let t={...f,page:e},a=await n.runAfterLoad(t,l.afterGuards);if(a.kind===`redirect`){r.debug(`popstate afterLoad → redirect to ${a.url}`);let t=++c;return u(a.url,0,t),e}if(a.kind===`rewrite`){r.debug(`popstate afterLoad → rewrite URL to ${a.url}`);let t=window.history.state?.id;return window.history.replaceState({id:t},``,a.url),i.onNavigate(new URL(a.url,window.location.origin).pathname),d(e),e}return a.kind===`deny`?(r.warn(`popstate afterLoad → denied (${a.status})`),e):(d(e),e)}),isFirstPage:s})});function d(e){(async()=>{try{e&&n.didEnterPage(e)}catch(e){r.error(`didEnterPage error:`,e)}})()}}function Gn(e){let{framework:t,log:n,callbacks:r,updateApp:i}=e;Wn({framework:t,log:n,callbacks:r,updateApp:i,getScrollablePageElement:e.getScrollablePageElement,manageHistory:e.manageHistory}),Bn({framework:t,log:n})}function Kn(){return document.getElementById(`scrollable-page-override`)||document.getElementById(`scrollable-page`)||document.documentElement}function qn(e){let{controller:t,codec:n,router:r,log:i}=e,a=new Un(i,{getScrollablePageElement:e.getScrollablePageElement??Kn,persistInHistoryState:!0}),o=!1,s=!0;t.subscribe(e=>{if(o)return;let t=n.encode(e.tree,r),c={tree:G(e.tree)},l=typeof window<`u`?window.location.pathname+window.location.search:t,u=s||t===l;a.beforeTransition(),u?a.replaceState(c,t):a.pushState(c,t),s=!1,i.debug(`[navigation] snapshot → ${u?`replace`:`push`} ${t}`)}),a.onPopState(async(e,n)=>{i.debug(`[navigation] popstate → ${e}, cached=${!!n}`);let r=c(e,n);if(r===void 0){i.warn(`[navigation] popstate: cannot restore tree for ${e}, keeping current`);return}o=!0;try{await t.hydrate(r)}finally{o=!1}});function c(e,t){if(t!==void 0)try{return zt(t.tree)}catch(e){i.warn(`[navigation] cached tree deserialize failed, falling back to codec`,e)}try{return n.decode(e,r)}catch(e){i.error(`[navigation] codec.decode failed:`,e);return}}return{getSnapshot(){return t.getSnapshot()},push(e,n,r){return t.push(e,n,r)},pop(e){return t.pop(e)},popToRoot(){return t.popToRoot()},replaceTop(e,n){return t.replaceTop(e,n)},selectTab(e,n){return t.selectTab(e,n)},selectColumn(e,n,r,i){return t.selectColumn(e,n,r,i)},hydrate(e){return t.hydrate(e)},subscribe(e){return t.subscribe(e)}}}function Jn(){let e=document.getElementById(`serialized-server-data`);if(e?.textContent){e.parentNode?.removeChild(e);try{return JSON.parse(e.textContent)}catch{return}}}function Yn(){let e=Jn();return!e||!Array.isArray(e)?j.empty():j.fromArray(e)}async function Xn(e){let{bootstrap:t,mountId:n=`app`,mount:r,callbacks:i,onBeforeStart:a,onAfterStart:o,frameworkConfig:s={},loadMessages:c}=e,l=Yn(),u=window.location.pathname+window.location.search,d=Qn(s.locale),f=await pe({locale:d,loadMessages:c,context:d?{runtime:`browser`,fetch:$n(s.fetch),url:u}:void 0}),p=it.create({...s,locale:d,_resolvedMessages:f,prefetchedIntents:l});t(p);let m=p.container.resolve(E.LOGGER_FACTORY).loggerFor(`browser`),h=p.getLocale();h&&(le(h),m.debug(`[startBrowserApp] Applied locale attributes:`,h)),await a?.(p);let g=await p.routeUrl(u),_=document.getElementById(n);if(!_)throw Error(`[startBrowserApp] Mount target not found: #${n}. Ensure your HTML has <div id="${n}"></div>.`);let v=r(_,{framework:p});if(Gn({framework:p,log:m,callbacks:i,updateApp:v,getScrollablePageElement:e.getScrollablePageElement,manageHistory:!e.navigation}),g?await p.perform(g.action):v({page:Promise.reject(Error(`404`)),isFirstPage:!0}),e.navigation){let t=await Zn({framework:p,navigation:e.navigation,log:m,getScrollablePageElement:e.getScrollablePageElement});await e.onNavigationReady?.(t)}await o?.(p)}async function Zn(e){let{framework:t,navigation:n,log:r,getScrollablePageElement:a}=e,o=n.codec??cn(),s=fn({intentDispatcher:t.intentDispatcher,router:t.router,initial:n.initial,createContext:({intent:e,params:n})=>{let r=o.encode({kind:`leaf`,intent:e,params:n},t.router);return{container:t.container,navigation:Z({url:r,intent:{id:e,params:n},container:t.container}),url:r}},beforeLoad:n.beforeLoad,afterLoad:n.afterLoad,prefetched:t.prefetchedIntents,getErrorPage:n.getErrorPage,onRedirect:({url:e})=>{t.perform(i(e))}}),c=qn({controller:s,codec:o,router:t.router,log:r,getScrollablePageElement:a});return await s.resolve(),c}function Qn(e){return e||document.documentElement.lang.trim()||void 0}function $n(e){return(e??globalThis.fetch?.bind(globalThis))||(()=>{throw Error(`[startBrowserApp] loadMessages requires a fetch implementation.`)})}export{Mt as $,ve as $t,bn as A,Je as At,zt as B,E as Bt,En as C,t as Cn,it as Ct,Cn as D,et as Dt,wn as E,tt as Et,Wt as F,Ze as Ft,B as G,Oe as Gt,Lt as H,Ae as Ht,cn as I,Ve as It,kt as J,Ce as Jt,Ot as K,we as Kt,ln as L,O as Lt,vn as M,Ge as Mt,Y as N,Ye as Nt,Sn as O,Qe as Ot,fn as P,He as Pt,Nt as Q,ye as Qt,Qt as R,ze as Rt,On as S,n as Sn,ot as St,Tn as T,i as Tn,A as Tt,wt as U,ke as Ut,G as V,Le as Vt,Ct as W,De as Wt,jt as X,S as Xt,Dt as Y,T as Yt,R as Z,be as Zt,Pn as _,c as _n,dt as _t,Gn as a,se as an,bt as at,X as b,e as bn,ct as bt,Hn as c,ce as cn,gt as ct,Rn as d,ee as dn,F as dt,ge as en,Pt as et,Ln as f,_ as fn,P as ft,Nn as g,u as gn,ft as gt,Mn as h,l as hn,mt as ht,qn as i,me as in,yt as it,yn as j,We as jt,xn as k,$e as kt,Bn as l,ue as ln,_t as lt,Fn as m,g as mn,pt as mt,Yn as n,_e as nn,L as nt,Wn as o,oe as on,I as ot,In as p,v as pn,ht as pt,At as q,Te as qt,Jn as r,pe as rn,xt as rt,Un as s,ae as sn,vt as st,Xn as t,he as tn,Tt as tt,zn as u,le as un,N as ut,Z as v,s as vn,at as vt,Dn as w,a as wn,j as wt,kn as x,r as xn,st as xt,jn as y,o as yn,M as yt,Zt as z,Re as zt};
2
+ //# sourceMappingURL=src-BI4eMjHk.mjs.map
@@ -0,0 +1,355 @@
1
+ # 11. Navigation
2
+
3
+ Chapters 2–4 cover the **flat single-page** lifecycle: one URL → one intent → one page. This chapter adds **structured navigation** — a recursive, UI-agnostic navigation tree analogous to SwiftUI's `NavigationStack`, `TabView`, and `NavigationSplitView`.
4
+
5
+ The framework owns navigation **state**, URL/history wiring, and per-destination intent dispatch. It ships **no UI**. Your `Page` models stay exactly as content-agnostic as before — you render tabs, stacks, and split views however you like with Svelte, React, or Vue.
6
+
7
+ A single-leaf tree is **byte-for-byte** the flat single-page behavior, so this is fully opt-in: apps that never call `defineNavigation` are unaffected.
8
+
9
+ ## The mental model
10
+
11
+ Navigation state is a tree of four node kinds:
12
+
13
+ ```
14
+ NavigationNode = LeafNode | StackNode | TabsNode | SplitNode
15
+ ```
16
+
17
+ | Node | Holds | Meaning | SwiftUI |
18
+ | ----------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------ | --------------------- |
19
+ | `LeafNode` | `intent` + `params` | One destination (one intent dispatch) | a destination view |
20
+ | `StackNode` | ordered `entries[]` | A path: `entries[0]` is the root, the last is the visible top | `NavigationStack` |
21
+ | `TabsNode` | `active` key + `branches` | Parallel branches; **only the active one is visible** | `TabView` |
22
+ | `SplitNode` | `columns[]` + optional `visibility` | Side-by-side columns; visible set is **all columns by default**, narrowable to `detailOnly` / `doubleColumn` | `NavigationSplitView` |
23
+
24
+ A leaf carries `intent` + `params`, **not** a `Page`. The tree is pure, serializable data describing _where_ to go; the controller produces _what's there_ (the `Page`) during resolution and hands it back in a snapshot. This is what keeps the tree URL- and history-friendly.
25
+
26
+ Interior nodes nest recursively — a `TabView` of `NavigationStack`s, a split whose detail column is a stack, and so on.
27
+
28
+ ## Declaring a tree
29
+
30
+ Constructors live alongside everything else in `@finesoft/front`:
31
+
32
+ ```ts
33
+ import { leaf, stack, tabs, split } from "@finesoft/front";
34
+
35
+ // A single destination — equivalent to today's flat page
36
+ leaf("home");
37
+ leaf("product", { id: 42 });
38
+
39
+ // A stack: root only, or root + already-pushed entries
40
+ stack(leaf("feed"));
41
+ stack([leaf("feed"), leaf("post", { id: 7 })]);
42
+
43
+ // Tabs: each branch is its own stack
44
+ tabs({
45
+ active: "home",
46
+ branches: {
47
+ home: stack(leaf("home")),
48
+ search: stack(leaf("search")),
49
+ me: stack(leaf("me")),
50
+ },
51
+ });
52
+
53
+ // Split: sidebar + detail, where detail is a stack
54
+ split([
55
+ { id: "sidebar", content: leaf("folders") },
56
+ { id: "detail", content: stack(leaf("folder", { id: "inbox" })) },
57
+ ]);
58
+ ```
59
+
60
+ `tabs()` derives a stable tab `order` from the `branches` insertion order unless you pass `order` explicitly. `stack()` accepts a single root node or an array of entries.
61
+
62
+ ## A TabView of NavigationStacks
63
+
64
+ The most common shape: a bottom tab bar where each tab keeps its own navigation depth.
65
+
66
+ ```ts
67
+ // src/bootstrap.ts
68
+ import { type Framework, defineRoutes, defineNavigation, leaf, stack, tabs } from "@finesoft/front";
69
+ import { HomeController } from "./lib/controllers/home";
70
+ import { SearchController } from "./lib/controllers/search";
71
+ import { ProfileController } from "./lib/controllers/profile";
72
+ import { PostController } from "./lib/controllers/post";
73
+
74
+ export function bootstrap(framework: Framework): void {
75
+ defineRoutes(framework, [
76
+ { path: "/", intentId: "home", controller: new HomeController() },
77
+ { path: "/search", intentId: "search", controller: new SearchController() },
78
+ { path: "/me", intentId: "me", controller: new ProfileController() },
79
+ { path: "/posts/:id", intentId: "post", controller: new PostController() },
80
+ ]);
81
+ }
82
+
83
+ // The navigation structure, declared once
84
+ export const navigation = defineNavigation({
85
+ initial: tabs({
86
+ active: "home",
87
+ branches: {
88
+ home: stack(leaf("home")),
89
+ search: stack(leaf("search")),
90
+ me: stack(leaf("me")),
91
+ },
92
+ }),
93
+ });
94
+ ```
95
+
96
+ `defineNavigation` returns a normalized definition with two adapters — `toBrowserConfig()` for CSR and `toSSRDefinition()` for SSR — so you declare the tree **once** and hand the right shape to each runner.
97
+
98
+ ### Wiring it into the browser
99
+
100
+ `startBrowserApp` gains an optional `navigation` field and an `onNavigationReady` callback that hands you a `NavigationHandle`:
101
+
102
+ ```ts
103
+ // src/main.ts
104
+ import { startBrowserApp, type NavigationHandle } from "@finesoft/front";
105
+ import { bootstrap, navigation } from "./bootstrap";
106
+ import { mount } from "./lib/mount";
107
+
108
+ let handle: NavigationHandle;
109
+
110
+ startBrowserApp({
111
+ bootstrap,
112
+ mount,
113
+ callbacks,
114
+ navigation: navigation.toBrowserConfig(),
115
+ onNavigationReady(h) {
116
+ handle = h;
117
+ // Re-render whenever the snapshot changes
118
+ h.subscribe((snapshot) => mountNavigation(snapshot));
119
+ mountNavigation(h.getSnapshot());
120
+ },
121
+ });
122
+ ```
123
+
124
+ When `navigation` is present, the framework builds a `NavigationController` and a history bridge, resolves the first screen, and gives you the handle. When it's absent, `startBrowserApp` runs the original flat single-page path unchanged.
125
+
126
+ ## Driving navigation
127
+
128
+ The `NavigationHandle` exposes the operations. Each returns a `Promise<NavigationSnapshot>` (the committed tree plus every visible destination's resolved `Page`) and, in the browser, writes the new state to history/URL.
129
+
130
+ ```ts
131
+ // Push a destination onto the active stack
132
+ await handle.push("post", { id: 7 });
133
+
134
+ // Pop back
135
+ await handle.pop(); // one level
136
+ await handle.pop(2); // two levels — never past the stack root
137
+ await handle.popToRoot();
138
+
139
+ // Replace the current top (e.g. login → dashboard without a back step)
140
+ await handle.replaceTop("dashboard");
141
+
142
+ // Switch the active tab — the other tabs keep their stack depth
143
+ await handle.selectTab("search");
144
+ ```
145
+
146
+ `pop` never pops below a stack's root entry. With no explicit target, stack operations act on the **deepest active stack** (the one currently visible), and `selectTab` acts on the **outermost** tabs node — exactly what you want for a tab bar driving the focused stack.
147
+
148
+ ### Reading the result
149
+
150
+ A `NavigationSnapshot` is what you render:
151
+
152
+ ```ts
153
+ const snapshot = handle.getSnapshot();
154
+ snapshot.tree; // the current NavigationNode tree
155
+ snapshot.destinations; // ResolvedDestination[]: { intent, params, page, status? }
156
+ ```
157
+
158
+ `destinations` is ordered to match `collectVisibleDestinations(tree)`: a tabs node contributes **only** its active branch, a split contributes **every** non-empty column. That ordering is also exactly what gets prefetched on the server.
159
+
160
+ Your view layer walks `snapshot.tree` to lay out the chrome (which tabs exist, how deep each stack is) and reads `snapshot.destinations` for the page content. The framework never tells you _how_ to draw any of it.
161
+
162
+ ## A NavigationSplitView
163
+
164
+ A split view shows multiple columns at once — the classic sidebar + detail (+ sub-detail) layout. Selecting in one column drives the next.
165
+
166
+ ```ts
167
+ export const navigation = defineNavigation({
168
+ initial: split([
169
+ { id: "sidebar", content: leaf("mailboxes") },
170
+ { id: "list", content: undefined }, // chosen later
171
+ { id: "detail", content: undefined },
172
+ ]),
173
+ });
174
+ ```
175
+
176
+ Use `selectColumn(columnId, intent, params?)` to set a column's content:
177
+
178
+ ```ts
179
+ // Pick a mailbox → fills the "list" column
180
+ await handle.selectColumn("list", "messages", { mailbox: "inbox" });
181
+
182
+ // Pick a message → fills the "detail" column
183
+ await handle.selectColumn("detail", "message", { id: 1024 });
184
+
185
+ // Re-pick a mailbox → clears "list" AND "detail" (everything after it)
186
+ await handle.selectColumn("list", "messages", { mailbox: "archive" });
187
+
188
+ // Clear a column explicitly by passing undefined for the intent
189
+ await handle.selectColumn("detail", undefined);
190
+ ```
191
+
192
+ Setting a column **clears every column after it**. Re-choosing the sidebar correctly invalidates the open detail, so you never render a stale "old detail with a new sidebar" combination.
193
+
194
+ By default every column is visible, so the snapshot's `destinations` contains one entry **per non-empty column** — the framework dispatches (and on the server, prefetches) each of them.
195
+
196
+ ### Column visibility
197
+
198
+ Mirroring SwiftUI's `NavigationSplitViewVisibility`, a split carries an optional **visibility** — bindable, serializable navigation state (not styling) that decides which columns count as visible, and therefore what gets prefetched on the server:
199
+
200
+ | `visibility` | Visible columns |
201
+ | ----------------------------- | ------------------------------- |
202
+ | `automatic` (default) / `all` | every column |
203
+ | `doubleColumn` | first + last (hides the middle) |
204
+ | `detailOnly` | last (detail) only |
205
+
206
+ ```ts
207
+ import { SPLIT_VISIBILITIES, visibleSplitColumns } from "@finesoft/front";
208
+
209
+ // Declare it up front (e.g. deep-link straight to the detail)
210
+ split(
211
+ [
212
+ { id: "sidebar", content: leaf("mailboxes") },
213
+ { id: "detail", content: leaf("message", { id: 7 }) },
214
+ ],
215
+ SPLIT_VISIBILITIES.DETAIL_ONLY,
216
+ );
217
+
218
+ // Or change it at runtime — newly-visible columns are dispatched, hidden ones are dropped from the snapshot
219
+ await handle.setVisibility(SPLIT_VISIBILITIES.DETAIL_ONLY); // only the detail destination remains
220
+ await handle.setVisibility(SPLIT_VISIBILITIES.ALL); // re-prefetches sidebar + list
221
+
222
+ // Render only the visible columns without re-implementing the mapping
223
+ for (const col of visibleSplitColumns(splitNode)) renderColumn(col);
224
+ ```
225
+
226
+ `detailOnly` deep-links resolve and prefetch **only** the detail column on the server — the hidden columns cost nothing until shown. Compact-width collapse (SwiftUI's `preferredCompactColumn`) is viewport-reactive rendering, so it stays entirely in your hands; read `getPlatform()` / the viewport and collapse the split into a stack view however you like.
227
+
228
+ ## Targeting a nested container
229
+
230
+ When a tree has more than one stack/tabs/split, pass an explicit `target` path to operate on a deeper one. A path is a list of steps from the root:
231
+
232
+ ```ts
233
+ import type { NavigationPath } from "@finesoft/front";
234
+
235
+ // The stack inside the detail column of a split
236
+ const detailStack: NavigationPath = [
237
+ { kind: "column", id: "detail" },
238
+ { kind: "stack-entry", index: 0 },
239
+ ];
240
+
241
+ await handle.push("attachment", { id: 3 }, { target: detailStack });
242
+ await handle.selectTab("photos", someTabsPath);
243
+ ```
244
+
245
+ Without a `target`, operations default to the active path, which is the right choice the vast majority of the time.
246
+
247
+ ## Pure operations (no controller needed)
248
+
249
+ Everything above is backed by pure, immutable tree functions you can use directly — for tests, optimistic computation, or building your own controller:
250
+
251
+ ```ts
252
+ import {
253
+ push,
254
+ pop,
255
+ selectTab,
256
+ collectVisibleDestinations,
257
+ resolveActivePath,
258
+ } from "@finesoft/front";
259
+
260
+ const next = push(tree, leaf("post", { id: 7 })); // returns a new tree
261
+ const visible = collectVisibleDestinations(next); // readonly LeafNode[]
262
+ const activePath = resolveActivePath(next);
263
+ ```
264
+
265
+ These never mutate their input — only the nodes on the changed path are rebuilt; the rest of the tree is shared by reference. Invalid targets (e.g. `selectTab` on a non-tabs node, popping an empty stack target) throw a `NavigationError`.
266
+
267
+ ## Server-side rendering
268
+
269
+ SSR prefetches **all** visible destinations and serializes them — plus the tree itself — into the HTML, so the browser's first render reuses the server result without refetching. Multi-column split views naturally prefetch multiple intents.
270
+
271
+ Use `createSSRNavigationRender` with the SSR adapter:
272
+
273
+ ```ts
274
+ // src/ssr.ts
275
+ import { createSSRNavigationRender } from "@finesoft/front";
276
+ import { bootstrap, navigation } from "./bootstrap";
277
+ import { renderApp } from "./lib/render";
278
+
279
+ export const render = createSSRNavigationRender({
280
+ bootstrap,
281
+ getErrorPage: (status, message) => ({
282
+ id: `error-${status}`,
283
+ pageType: "error",
284
+ title: message,
285
+ }),
286
+ renderApp, // (page, framework, snapshot) => { html, head, css }
287
+ navigation: navigation.toSSRDefinition(),
288
+ });
289
+ ```
290
+
291
+ `renderApp` receives three arguments: the **primary** page (the active leaf's result — compatible with the flat SSR `renderApp` signature), the framework, and the full multi-region `snapshot` so you can render tabs/split layouts:
292
+
293
+ ```ts
294
+ function renderApp(page, framework, snapshot) {
295
+ // page → the focused destination (e.g. for <title>, status)
296
+ // snapshot.tree → which tabs/columns to draw
297
+ // snapshot.destinations → the Page for each visible region
298
+ return renderYourFramework(snapshot);
299
+ }
300
+ ```
301
+
302
+ How it works under the hood: each visible destination is serialized through the **existing** `PrefetchedIntents` channel as a normal `{ intent, data: page }` entry, plus one sentinel entry carrying the serialized tree. `@finesoft/server` needs **zero changes** — it transports the sentinel through the same `#serialized-server-data` script. On hydration the browser bridge reads the tree back from history state (or the sentinel) and reuses the prefetched pages.
303
+
304
+ If a request has no structural deep-link and your app provides no skeleton, SSR falls back to `Router.resolve(url)` → a single leaf — i.e. today's flat single page, including its `renderMode`. The 404 path is unchanged.
305
+
306
+ ## Deep-linking with `createFullStateCodec`
307
+
308
+ By default, the **active leaf** drives the URL (`/posts/7`) and the full tree travels via history state — clean, shareable URLs for the focused destination. To encode the **entire** tree into the URL for full deep-linking (sharing a link that restores tabs, stack depth, and split selections), opt into `createFullStateCodec`:
309
+
310
+ ```ts
311
+ import { createFullStateCodec } from "@finesoft/front";
312
+
313
+ export const navigation = defineNavigation({
314
+ initial: tabs({
315
+ active: "home",
316
+ branches: { home: stack(leaf("home")), me: stack(leaf("me")) },
317
+ }),
318
+ codec: createFullStateCodec(), // whole tree → "?__nav=..." query param
319
+ });
320
+ ```
321
+
322
+ Now URLs look like `/me?__nav=<encoded-tree>`, and pasting one restores the complete navigation state on both SSR and the browser. The encoding is compact (base64url), stable (sorted keys, so the same tree always yields the same string), and lossless. Pass `createFullStateCodec({ param: "nav" })` to rename the reserved query parameter.
323
+
324
+ You can also implement a custom `NavigationCodec` if you need a bespoke URL scheme — both built-ins only depend on the router's `getRoutes()` (and optional `reverse()`), nothing more.
325
+
326
+ ## Guards still work
327
+
328
+ Navigation-level `beforeLoad` / `afterLoad` guards run for the **primary** destination (the active leaf) on every navigation, with the same `redirect` / `rewrite` / `deny` semantics as [chapter 3](./03-middleware.md):
329
+
330
+ ```ts
331
+ export const navigation = defineNavigation({
332
+ initial: tabs({
333
+ active: "home",
334
+ branches: { home: stack(leaf("home")), me: stack(leaf("me")) },
335
+ }),
336
+ beforeLoad: [authGuard],
337
+ });
338
+ ```
339
+
340
+ - `redirect` → handled as an in-app navigation (the browser reuses the FlowAction pipeline); the target isn't dispatched.
341
+ - `rewrite` → the new URL is re-resolved into the destination's intent/params.
342
+ - `deny` → the destination is marked with the deny status and its intent is not dispatched.
343
+
344
+ A single destination's dispatch failure never throws out of an operation — it records a `status` and a fallback page on that destination (the same `fallback` safety net as controllers), so one failing split column can't blank the whole screen.
345
+
346
+ ## Backward compatibility
347
+
348
+ - Apps that don't pass `navigation` to `startBrowserApp` / `createSSRRender` run the **original flat path** with zero behavior change.
349
+ - A single-leaf tree is equivalent to the flat single page: one visible destination, one resolve/dispatch, one before/after pass. SSR only adds the tree sentinel to `serverData` (stripped before it reaches `PrefetchedIntents`).
350
+ - `Page` stays content-agnostic. Navigation adds structure _around_ your pages; it never dictates their shape or how you render them.
351
+
352
+ ## Next
353
+
354
+ - [Middleware](./03-middleware.md) — the guard semantics navigation reuses
355
+ - [Rendering & hydration](./04-rendering-and-hydration.md) — how prefetched results cross the SSR → CSR boundary