@livenetworks/ashlar 1.5.1 → 1.6.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.
Files changed (217) hide show
  1. package/README.md +189 -177
  2. package/js/COMPONENTS.md +55 -49
  3. package/js/index.js +22 -5
  4. package/js/ln-accordion/README.md +186 -137
  5. package/js/ln-accordion/ln-accordion.js +1 -1
  6. package/js/ln-ajax/README.md +73 -4
  7. package/js/ln-ajax/ln-ajax.js +1 -1
  8. package/js/ln-ajax/src/ln-ajax.js +13 -13
  9. package/js/ln-api-connector/README.md +261 -150
  10. package/js/ln-api-connector/ln-api-connector.js +1 -1
  11. package/js/ln-api-connector/src/ln-api-connector.js +368 -265
  12. package/js/ln-api-queue/README.md +219 -0
  13. package/js/ln-api-queue/ln-api-queue.js +1 -0
  14. package/js/ln-api-queue/src/ln-api-queue.js +320 -0
  15. package/js/ln-api-queue/src/queue-storage.js +455 -0
  16. package/js/ln-autoresize/README.md +47 -0
  17. package/js/ln-autoresize/ln-autoresize.js +1 -1
  18. package/js/ln-autosave/README.md +33 -1
  19. package/js/ln-autosave/ln-autosave.js +1 -1
  20. package/js/ln-autosave/src/ln-autosave.js +4 -4
  21. package/js/ln-circular-progress/README.md +27 -13
  22. package/js/ln-circular-progress/ln-circular-progress.js +1 -1
  23. package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
  24. package/js/ln-confirm/README.md +162 -86
  25. package/js/ln-confirm/_ln-confirm.scss +11 -0
  26. package/js/ln-confirm/ln-confirm.js +1 -1
  27. package/js/ln-confirm/src/ln-confirm.js +77 -41
  28. package/js/ln-core/README.md +896 -0
  29. package/js/ln-core/hash.js +92 -0
  30. package/js/ln-core/helpers.js +337 -8
  31. package/js/ln-core/index.js +4 -2
  32. package/js/ln-core/persist.js +29 -3
  33. package/js/ln-core/positioning.js +1 -35
  34. package/js/ln-core/window-cache.js +213 -0
  35. package/js/ln-couchdb-connector/README.md +177 -156
  36. package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
  37. package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
  38. package/js/ln-data-coordinator/README.md +536 -165
  39. package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
  40. package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
  41. package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
  42. package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
  43. package/js/ln-data-store/README.md +241 -94
  44. package/js/ln-data-store/ln-data-store.js +1 -1
  45. package/js/ln-data-store/src/ln-data-store.js +694 -699
  46. package/js/ln-date/README.md +39 -1
  47. package/js/ln-date/ln-date-dev.scss +14 -0
  48. package/js/ln-date/ln-date.js +1 -1
  49. package/js/ln-date/src/ln-date.js +236 -178
  50. package/js/ln-debug/README.md +58 -0
  51. package/js/ln-debug/ln-debug.js +1 -0
  52. package/js/ln-debug/src/ln-debug.js +19 -0
  53. package/js/ln-dropdown/README.md +31 -8
  54. package/js/ln-dropdown/ln-dropdown.js +1 -1
  55. package/js/ln-dropdown/src/ln-dropdown.js +30 -13
  56. package/js/ln-editor/README.md +274 -0
  57. package/js/ln-editor/ln-editor.js +1 -0
  58. package/js/ln-editor/ln-editor.scss +43 -0
  59. package/js/ln-editor/src/ln-editor.js +619 -0
  60. package/js/ln-external-links/README.md +44 -5
  61. package/js/ln-external-links/ln-external-links.js +1 -1
  62. package/js/ln-fill/README.md +199 -0
  63. package/js/ln-fill/ln-fill.js +1 -0
  64. package/js/ln-fill/src/ln-fill.js +87 -0
  65. package/js/ln-filter/README.md +92 -2
  66. package/js/ln-filter/ln-filter-dev.scss +8 -0
  67. package/js/ln-filter/ln-filter.js +1 -1
  68. package/js/ln-filter/src/ln-filter.js +22 -7
  69. package/js/ln-form/README.md +212 -49
  70. package/js/ln-form/ln-form.js +1 -1
  71. package/js/ln-form/src/ln-form.js +99 -199
  72. package/js/ln-http/README.md +125 -89
  73. package/js/ln-http/ln-http.js +1 -1
  74. package/js/ln-http/src/ln-http.js +32 -13
  75. package/js/ln-icon/README.md +130 -0
  76. package/js/ln-icon/ln-icon.js +1 -0
  77. package/js/{ln-icons/src/ln-icons.js → ln-icon/src/ln-icon.js} +20 -15
  78. package/js/ln-link/README.md +47 -9
  79. package/js/ln-link/ln-link.js +1 -1
  80. package/js/ln-link/src/ln-link.js +22 -6
  81. package/js/ln-list/README.md +129 -0
  82. package/js/ln-list/ln-list.js +1 -0
  83. package/js/ln-list/ln-list.scss +28 -0
  84. package/js/ln-list/src/ln-list.js +1216 -0
  85. package/js/ln-modal/README.md +185 -154
  86. package/js/ln-modal/ln-modal-dev.scss +13 -0
  87. package/js/ln-modal/ln-modal.js +1 -1
  88. package/js/ln-modal/ln-modal.scss +28 -4
  89. package/js/ln-modal/src/ln-modal.js +137 -201
  90. package/js/ln-modal-coordinator/README.md +99 -0
  91. package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +8 -0
  92. package/js/ln-modal-coordinator/ln-modal-coordinator.js +1 -0
  93. package/js/ln-modal-coordinator/src/ln-modal-coordinator.js +345 -0
  94. package/js/ln-nav/README.md +46 -3
  95. package/js/ln-nav/ln-nav.js +1 -1
  96. package/js/ln-nav/src/ln-nav.js +92 -127
  97. package/js/ln-number/README.md +28 -1
  98. package/js/ln-number/ln-number-dev.scss +13 -0
  99. package/js/ln-number/ln-number.js +1 -1
  100. package/js/ln-number/src/ln-number.js +443 -350
  101. package/js/ln-options/README.md +66 -0
  102. package/js/ln-options/ln-options.js +1 -0
  103. package/js/ln-options/src/ln-options.js +74 -0
  104. package/js/ln-popover/README.md +40 -5
  105. package/js/ln-popover/ln-popover.js +1 -1
  106. package/js/ln-popover/src/ln-popover.js +38 -11
  107. package/js/ln-progress/README.md +413 -442
  108. package/js/ln-progress/ln-progress.js +1 -1
  109. package/js/ln-progress/src/ln-progress.js +98 -150
  110. package/js/ln-router/README.md +432 -0
  111. package/js/ln-router/ln-router.js +1 -0
  112. package/js/ln-router/src/ln-router.js +539 -0
  113. package/js/ln-search/README.md +158 -83
  114. package/js/ln-search/ln-search-dev.scss +8 -0
  115. package/js/ln-search/ln-search.js +1 -1
  116. package/js/ln-search/src/ln-search.js +12 -9
  117. package/js/ln-slug/README.md +110 -0
  118. package/js/ln-slug/ln-slug.js +1 -0
  119. package/js/ln-slug/src/ln-slug.js +74 -0
  120. package/js/ln-sort/README.md +170 -0
  121. package/js/ln-sort/ln-sort-dev.scss +8 -0
  122. package/js/ln-sort/ln-sort.js +1 -0
  123. package/js/ln-sort/src/ln-sort.js +178 -0
  124. package/js/ln-sortable/README.md +133 -95
  125. package/js/ln-sortable/ln-sortable.js +1 -1
  126. package/js/ln-stat/README.md +55 -0
  127. package/js/ln-stat/ln-stat.js +1 -0
  128. package/js/ln-stat/src/ln-stat.js +55 -0
  129. package/js/ln-table/README.md +285 -101
  130. package/js/ln-table/ln-table-dev.scss +17 -0
  131. package/js/ln-table/ln-table.js +1 -1
  132. package/js/ln-table/ln-table.scss +1 -1
  133. package/js/ln-table/src/ln-table.js +1493 -473
  134. package/js/ln-table-coordinator/README.md +73 -0
  135. package/js/ln-table-coordinator/ln-table-coordinator.js +1 -0
  136. package/js/ln-table-coordinator/src/ln-table-coordinator.js +160 -0
  137. package/js/ln-tabs/README.md +221 -137
  138. package/js/ln-tabs/ln-tabs-dev.scss +18 -0
  139. package/js/ln-tabs/ln-tabs.js +1 -1
  140. package/js/ln-tabs/src/ln-tabs.js +41 -24
  141. package/js/ln-time/README.md +14 -0
  142. package/js/ln-time/ln-time.js +1 -1
  143. package/js/ln-time/src/ln-time.js +223 -192
  144. package/js/ln-toast/README.md +178 -122
  145. package/js/ln-toast/ln-toast.js +1 -15
  146. package/js/ln-toast/src/ln-toast.js +232 -210
  147. package/js/ln-toast/template.html +16 -12
  148. package/js/ln-toggle/README.md +61 -2
  149. package/js/ln-toggle/ln-toggle-dev.scss +13 -0
  150. package/js/ln-toggle/ln-toggle.js +1 -1
  151. package/js/ln-toggle/src/ln-toggle.js +56 -43
  152. package/js/ln-tooltip/README.md +95 -58
  153. package/js/ln-tooltip/ln-tooltip-dev.scss +9 -0
  154. package/js/ln-tooltip/ln-tooltip.js +1 -1
  155. package/js/ln-tooltip/src/ln-tooltip.js +45 -6
  156. package/js/ln-translations/README.md +118 -96
  157. package/js/ln-translations/ln-translations.js +1 -1
  158. package/js/ln-translations/src/ln-translations.js +10 -5
  159. package/js/ln-upload/README.md +85 -10
  160. package/js/ln-upload/ln-upload.js +1 -1
  161. package/js/ln-upload/src/ln-upload.js +26 -10
  162. package/js/ln-validate/README.md +72 -0
  163. package/js/ln-validate/ln-validate-dev.scss +15 -0
  164. package/js/ln-validate/ln-validate.js +1 -1
  165. package/js/ln-validate/src/ln-validate.js +64 -0
  166. package/package.json +61 -56
  167. package/scss/base/_global.scss +2 -2
  168. package/scss/base/_typography.scss +1 -2
  169. package/scss/components/_date.scss +1 -0
  170. package/scss/components/_dropdown.scss +0 -5
  171. package/scss/components/_editor.scss +5 -0
  172. package/scss/components/_filter.scss +26 -25
  173. package/scss/components/_form.scss +7 -0
  174. package/scss/components/_ln-table.scss +13 -3
  175. package/scss/components/_sort.scss +10 -0
  176. package/scss/components/_table.scss +36 -23
  177. package/scss/components/_toast.scss +68 -41
  178. package/scss/components/_toggle.scss +16 -0
  179. package/scss/config/_density.scss +205 -117
  180. package/scss/config/_icons.scss +10 -9
  181. package/scss/config/_tokens.scss +69 -47
  182. package/scss/config/mixins/_accordion.scss +2 -7
  183. package/scss/config/mixins/_alert.scss +4 -4
  184. package/scss/config/mixins/_breadcrumbs.scss +2 -2
  185. package/scss/config/mixins/_btn.scss +1 -1
  186. package/scss/config/mixins/_card.scss +89 -14
  187. package/scss/config/mixins/_collapsible.scss +3 -0
  188. package/scss/config/mixins/_colors.scss +1 -1
  189. package/scss/config/mixins/_diagnostics.scss +11 -0
  190. package/scss/config/mixins/_display.scss +1 -0
  191. package/scss/config/mixins/_dropdown.scss +3 -5
  192. package/scss/config/mixins/_editor.scss +124 -0
  193. package/scss/config/mixins/_empty-state.scss +7 -0
  194. package/scss/config/mixins/_filter.scss +3 -5
  195. package/scss/config/mixins/_footer.scss +1 -2
  196. package/scss/config/mixins/_form.scss +207 -21
  197. package/scss/config/mixins/_index.scss +2 -0
  198. package/scss/config/mixins/_ln-table.scss +41 -2
  199. package/scss/config/mixins/_modal.scss +149 -66
  200. package/scss/config/mixins/_nav.scss +3 -4
  201. package/scss/config/mixins/_popover.scss +39 -0
  202. package/scss/config/mixins/_sidebar.scss +1 -1
  203. package/scss/config/mixins/_sort.scss +42 -0
  204. package/scss/config/mixins/_table.scss +85 -60
  205. package/scss/config/mixins/_toast.scss +179 -152
  206. package/scss/config/mixins/_tooltip.scss +17 -7
  207. package/scss/config/mixins/_typography.scss +4 -2
  208. package/scss/ln-ashlar-dev.scss +27 -9
  209. package/scss/ln-ashlar.scss +7 -1
  210. package/js/ln-data-table/README.md +0 -110
  211. package/js/ln-data-table/ln-data-table.js +0 -1
  212. package/js/ln-data-table/ln-data-table.scss +0 -10
  213. package/js/ln-data-table/src/ln-data-table.js +0 -1087
  214. package/js/ln-icons/README.md +0 -88
  215. package/js/ln-icons/ln-icons.js +0 -1
  216. package/js/ln-table/ln-table-sort.js +0 -1
  217. package/js/ln-table/src/ln-table-sort.js +0 -156
@@ -0,0 +1,432 @@
1
+ # ln-router
2
+
3
+ > A zero-dependency, template-driven, client-side SPA router that maps URLs to route templates and renders them declaratively.
4
+
5
+ ---
6
+
7
+ ## 1. Philosophy & The Primitive Mindset
8
+
9
+ In `ln-ashlar`, the core design principle is **orthogonality**. Rather than creating a heavy router that handles data fetching, state management, and HTML sanitization, `ln-router` separates these concerns:
10
+
11
+ 1. **The Route Mounter (JavaScript)**: The `ln-router` component only manages path matching, history states (`pushState`/`popstate`), accessibility focus shifting, and DOM swapping. It does not fetch data, sanitise markup, or touch database stores.
12
+ 2. **The Declarative Layout (HTML)**: Route configurations and view templates live in HTML as `<template data-ln-route="...">` tags. The router never constructs HTML inside JS. It simply clones the templates into the designated outlet.
13
+ 3. **Decoupled Data Binding (Coordinator)**: The coordinator listens to the `ln-router:navigated` event, retrieves params/query, fetches from a store or HTTP endpoint, and updates the mounted view using core helpers:
14
+ - **`fillTemplate(clone, data)`**: Replaces inline `{{ placeholder }}` text-node variables in the cloned view. Since it operates strictly on text nodes (via `TreeWalker`), it is 100% safe from reflected XSS.
15
+ - **`fill(el, data)`**: Performs attribute-driven binding (`data-ln-field` → `textContent`, `data-ln-attr` → `setAttribute`), which is also inherently secure.
16
+
17
+ > [!WARNING]
18
+ > **Reflected XSS Security**: The route coordinator is responsible for populating the view. When binding URL parameters (e.g., username in `/profile/:username`) or query parameters to the DOM, **never** use unsafe methods like `innerHTML` or `outerHTML`. Always use `fillTemplate`, `fill`, or set `textContent` directly to avoid reflected XSS injections. For more, see [Security Architecture](file:///c:/laragon/www/ln-ashlar/docs/architecture/security.md#safe-dom-interpolation).
19
+
20
+ ---
21
+
22
+ ## 2. Minimal Blueprint
23
+
24
+ Route templates and outlets are declared in HTML. Triggers are plain same-origin `<a>` tags.
25
+
26
+ ```html
27
+ <!-- Default Outlet (where views render by default) -->
28
+ <main data-ln-outlet></main>
29
+
30
+ <!-- Route Templates (lives anywhere, commonly grouped near outlet) -->
31
+ <template data-ln-route="/">
32
+ <article>
33
+ <h1>Home</h1>
34
+ <p>Welcome to Ashlar SPA.</p>
35
+ </article>
36
+ </template>
37
+
38
+ <template data-ln-route="/users">
39
+ <article>
40
+ <h1>Users List</h1>
41
+ <ul id="user-list"></ul>
42
+ </article>
43
+ </template>
44
+
45
+ <!-- Parameter route -->
46
+ <template data-ln-route="/users/:id" data-ln-route-title="User Details">
47
+ <article>
48
+ <h1 id="user-name-title">Loading User...</h1>
49
+ </article>
50
+ </template>
51
+
52
+ <!-- Catch-all fallback route -->
53
+ <template data-ln-route="*">
54
+ <article>
55
+ <h1>404 - Not Found</h1>
56
+ </article>
57
+ </template>
58
+ ```
59
+
60
+ ### Multi-Region Blueprint
61
+
62
+ ```html
63
+ <!-- Primary outlet -->
64
+ <main data-ln-outlet></main>
65
+
66
+ <!-- Persistent sidebar — keeps state on param-only changes -->
67
+ <aside id="side" data-ln-route-keep></aside>
68
+
69
+ <!-- Primary region templates -->
70
+ <template data-ln-route="/users/:id">
71
+ <article><h1>User {{id}}</h1></article>
72
+ </template>
73
+
74
+ <!-- Sidebar templates -->
75
+ <template data-ln-route="/users/:id" data-ln-route-target="side">
76
+ <section>
77
+ <h2>Sidebar: User {{id}}</h2>
78
+ <input type="text" placeholder="Type something — survives param changes">
79
+ </section>
80
+ </template>
81
+ ```
82
+
83
+ ---
84
+
85
+ ## 3. The Declarative API & State Contract
86
+
87
+ There is no need to manually instantiate or initialize the router. Discovered `<template data-ln-route>` elements register themselves automatically.
88
+
89
+ ```js
90
+ import { router } from '../ln-router';
91
+
92
+ // Programmatic navigation
93
+ router.navigate('/users/42?tab=profile');
94
+
95
+ // Programmatic replace state
96
+ router.replace('/dashboard');
97
+
98
+ // Read-only state query
99
+ const current = router.current();
100
+ // Returns:
101
+ // {
102
+ // path: '/users/42?tab=profile',
103
+ // params: { id: '42' }, // primary region params
104
+ // query: { tab: 'profile' },
105
+ // route: {
106
+ // pattern: '/users/:id',
107
+ // target: null, // The string ID of the custom target outlet, or null if using default
108
+ // title: 'User Details',
109
+ // templateNode: HTMLTemplateElement
110
+ // },
111
+ // regions: Map { // NEW — per-region snapshot
112
+ // '__primary__' => { route: {...}, params: { id: '42' } },
113
+ // 'side' => { route: {...}, params: { id: '42' } } | null
114
+ // }
115
+ // }
116
+ ```
117
+
118
+ ### Target Outlet Resolution Fallback
119
+ When a route matches, the router resolves the target DOM element to render into by checking the following hierarchy:
120
+ 1. An explicit element ID specified by `data-ln-route-target="..."` on the `<template>`.
121
+ 2. A default outlet element marked with the `data-ln-outlet` attribute.
122
+ 3. The first `<main>` element found in the document.
123
+
124
+ If the **primary** outlet cannot be resolved, the router logs a `console.warn` and aborts the navigation, leaving the current page state intact. If an **auxiliary** region's target element cannot be resolved, the router warns and skips only that region — the remaining regions still render.
125
+
126
+ ### Attributes
127
+ - `data-ln-route="pattern"`: Declares a route on a `<template>`. Supports static segments (`/users`), parameters (`/users/:id`), and wildcards (`*`).
128
+ - `data-ln-route-target="id"`: Sets an explicit element ID to render this route into. Templates with no `data-ln-route-target` belong to the **primary region** (default outlet).
129
+ - `data-ln-route-title="text"`: Sets `document.title` on successful navigation (primary region only).
130
+ - `data-ln-outlet`: Marks the default outlet element (falls back to the first `<main>` element if omitted).
131
+ - `data-ln-router-hydrate`: Placed on the outlet element to signal that initial SSR (Server-Side Rendered) content is already present, letting the router skip the initial clone and register events cleanly.
132
+ - `data-ln-route-keep`: Placed on a **target element** (not a template). When present, the router skips teardown and DOM swap for that region if the matched template is the same as the one currently mounted — surviving param-only URL changes while preserving DOM state (inputs, scroll, open panels). When the matched template changes, teardown and swap proceed normally. Ignored on the primary outlet (the primary always re-renders).
133
+
134
+ ---
135
+
136
+ ## 4. Transition Events
137
+
138
+ All events bubble from the target outlet element.
139
+
140
+ | Event | Cancelable | Detail | Dispatched When |
141
+ |---|---|---|---|
142
+ | **`ln-router:before-navigate`** | **Yes** | `{ from, to, params, query }` | Fired **once per navigation** on the primary outlet before any region swap. `preventDefault()` aborts the entire navigation (no history update, no region swaps). |
143
+ | **`ln-router:navigated`** | No | `{ path, params, query, route, target, region }` | Fired **per region that actually swapped**. `region` is `'__primary__'` for the default outlet, or the target element's id string for auxiliary regions. All existing keys (`path, params, query, route, target`) remain present for backward compatibility. |
144
+ | **`ln-router:not-found`** | No | `{ path }` | When the primary region has no match (and no catch-all `*` exists). DOM is left untouched. Not fired on auxiliary-only pages. |
145
+
146
+ ### Timing guarantee
147
+
148
+ The initial render runs after the full route scan completes — all `<template data-ln-route>` elements in the document are registered before the first navigation fires.
149
+ The initial `ln-router:navigated` (on a matched route) and boot-path `ln-router:not-found` (on no match) dispatches are **deferred one microtask** via `queueMicrotask`. This ensures that listeners registered during the same `DOMContentLoaded` burst — i.e. in a `<script defer>` tag that appears after the router bundle — always receive the boot event. When the View Transitions API is available, the deferral is handled via the VT callback rather than literally `queueMicrotask`, but the guarantee is identical — listeners in a later `defer` script still receive the boot event.
150
+
151
+ All subsequent navigations (link clicks, `popstate`, programmatic `router.navigate()`/`router.replace()`) dispatch **synchronously**; no deferral applies once the boot phase is complete.
152
+
153
+ **Consumers no longer need a `current()` boot-replay block.** The pattern:
154
+
155
+ ```js
156
+ // OBSOLETE — no longer needed
157
+ const booted = window.lnRouter && window.lnRouter.current();
158
+ if (booted && !navigatedFired) {
159
+ mountRoute(booted);
160
+ }
161
+ ```
162
+
163
+ can be replaced by simply attaching a `ln-router:navigated` listener — it will fire even if the router booted before the listener was added.
164
+
165
+ ```js
166
+ // Example: Block navigation out of unsaved forms
167
+ document.addEventListener('ln-router:before-navigate', (e) => {
168
+ if (hasUnsavedChanges && !confirm('Discard changes?')) {
169
+ e.preventDefault(); // Cancels transition and keeps current view
170
+ }
171
+ });
172
+ ```
173
+
174
+ ## 5. Fragment-only popstate guard
175
+
176
+ A `popstate` event whose new URL differs from the current SPA state **only in
177
+ the fragment** (path and query are identical) is **skipped** — no navigation,
178
+ no outlet teardown, no `ln-router:navigated` event fires.
179
+
180
+ This is what makes hash-bound overlays (like `ln-modal`, which uses its
181
+ element `id` as the hash namespace) work cleanly with Back/Forward:
182
+
183
+ 1. User opens a hash-bound modal → URL gets `#id:value`.
184
+ 2. User presses **Back** → browser fires `popstate` to the URL without the
185
+ fragment.
186
+ 3. ln-router detects path + query are identical → **skips navigation** → the
187
+ under-modal view (table, scroll position, search/filter state) is
188
+ completely preserved.
189
+ 4. `hashchange` fires → ln-modal's `_onHashChange` sees the segment is gone
190
+ → `data-ln-modal="close"` → modal closes cleanly.
191
+
192
+ Real path/query Back/Forward navigations and link clicks are unaffected. The
193
+ `router.current()` return value is left coherent (unchanged) on a skipped
194
+ fragment popstate — no stale state is introduced.
195
+
196
+ This guard is implemented via `_queryEqual` in the router source.
197
+
198
+ ## 6. Accessibility & Screen Reader Continuity
199
+
200
+ `ln-router` shifts focus automatically on each user-initiated navigation (click or popstate back/forward) to ensure compatibility with assistive technologies (screen readers). Focus is **not** shifted on the initial page load (boot render) to avoid an unexpected visible focus ring without user action.
201
+ 1. **Focus Shifting** (user navigation only): The router looks for the first heading element (`h1` through `h6`) in the newly mounted template. If found, it dynamically assigns `tabindex="-1"` and calls `.focus()` on it. If no heading element is present, focus is shifted to the outlet container itself.
202
+ 2. **Title Updates**: When `data-ln-route-title` is defined, the router updates `document.title`, which is announced by screen readers upon focus shifting.
203
+ 3. **Scroll Management** (user navigation only): The router automatically scrolls the target element into view via `.scrollIntoView({ block: 'start', behavior: 'instant' })` to reset the viewport for the new content.
204
+
205
+ ---
206
+
207
+ ## 7. Server Integration & Deep Links
208
+
209
+ Since `ln-router` operates client-side using history `pushState`, direct access or page refresh on a deep path (e.g., direct opening of `/users/42`) will bypass the client-side router and hit the web server directly. This results in a server 404 error if not configured correctly.
210
+
211
+ ### 1. Server Catch-All Fallback
212
+ Your web server must serve the main shell entrypoint (e.g., `index.html` or main layout view) for all deep URLs, allowing the client-side router to boot and handle the path.
213
+
214
+ In **Laravel**, define a fallback route at the end of `routes/web.php`:
215
+ ```php
216
+ Route::fallback(function () {
217
+ return view('app'); // returns your main blade layout shell
218
+ });
219
+ ```
220
+
221
+ In **Nginx**, configure the fallback in your site configuration:
222
+ ```nginx
223
+ location / {
224
+ try_files $uri $uri/ /index.html;
225
+ }
226
+ ```
227
+
228
+ ### 2. Relative Assets & `<base>` Tag
229
+ If your app references assets using relative paths, deep paths will attempt to load assets relative to the current route path (e.g., loading `/users/assets/app.js` instead of `/assets/app.js`). To prevent this:
230
+ - **Configure the base tag**: Add `<base href="/">` in the `<head>` of your main HTML shell.
231
+ - **Or use absolute asset paths**: Ensure script and link tags point to absolute URLs (e.g., `/dist/main.js` instead of `dist/main.js`).
232
+
233
+ ---
234
+
235
+ ## 8. Routing Model — Parallel Regions, Not Nested Routes
236
+
237
+ > [!NOTE]
238
+ > **No Nested Routing**: `ln-router` does not support nested outlets or child routes (rendering a sub-view inside an already-routed parent). All routes are flat.
239
+
240
+ > [!NOTE]
241
+ > **Multi-region routing is PARALLEL (sibling), not nested.** When a single URL simultaneously paints a sidebar region and a main region, both regions are siblings at the page level — neither is a child of the other's outlet. The "No Nested Routing" doctrine applies: there is no concept of a parent route whose outlet contains another router outlet.
242
+
243
+ ---
244
+
245
+ ## 9. Integration Patterns
246
+
247
+ ### Coordinator Data Binding (Secure Interpolation)
248
+ Coordinators listen to `ln-router:navigated`, fetch data, and populate the view. Use `fillTemplate` for safe `{{ placeholder }}` text-node substitutions:
249
+
250
+ ```html
251
+ <template data-ln-route="/profile/:username">
252
+ <section>
253
+ <h1>Profile: {{username}}</h1>
254
+ </section>
255
+ </template>
256
+ ```
257
+
258
+ ```js
259
+ import { fillTemplate } from '../ln-core';
260
+
261
+ document.addEventListener('ln-router:navigated', function (e) {
262
+ if (e.detail.route.pattern !== '/profile/:username') return;
263
+
264
+ const username = e.detail.params.username;
265
+ // Populate read-only placeholders like {{username}} inside the outlet
266
+ // fillTemplate walks text nodes and is completely safe from XSS.
267
+ fillTemplate(e.detail.target, { username: username });
268
+ });
269
+ ```
270
+
271
+ ---
272
+
273
+ ## 10. Integration & Source Files
274
+
275
+ `ln-ashlar` is a source-only package. Products should bundle it from source using their asset compilation pipelines (Vite, Laravel Mix, Webpack, etc.):
276
+
277
+ - **Vite / ES Modules**:
278
+ ```js
279
+ import '@livenetworks/ashlar/js/ln-router';
280
+ ```
281
+ - **Git Submodule ESM import**:
282
+ ```js
283
+ import 'resources/ln-ashlar/js/ln-router/src/ln-router.js';
284
+ ```
285
+ - **Development Demo**:
286
+ The compiled build in `demo/dist/ln-ashlar.iife.js` exists solely for running the local repository demo environment and should not be referenced in production consumer applications.
287
+
288
+ ---
289
+
290
+ ## 11. Multi-Region Routing
291
+
292
+ One URL can simultaneously paint multiple sibling regions. Each region independently maintains its own route table and matches the current URL.
293
+
294
+ ### Declaring regions
295
+
296
+ Regions are ordinary DOM elements. Each is identified by an `id`. Route templates target a region via `data-ln-route-target="<id>"`. Templates with no `data-ln-route-target` belong to the **primary region** (the default outlet).
297
+
298
+ ```html
299
+ <!-- Primary outlet (default region) -->
300
+ <main data-ln-outlet></main>
301
+
302
+ <!-- Auxiliary region: sidebar -->
303
+ <aside id="side" data-ln-route-keep></aside>
304
+
305
+ <!-- Primary templates -->
306
+ <template data-ln-route="/users/:id" data-ln-route-title="User Details">
307
+ <article><h1>User {{id}}</h1></article>
308
+ </template>
309
+
310
+ <!-- Sidebar templates — same or different patterns -->
311
+ <template data-ln-route="/users/:id" data-ln-route-target="side">
312
+ <section><h2>Sidebar for User {{id}}</h2></section>
313
+ </template>
314
+
315
+ <!-- Sidebar catch-all — shown on all other routes -->
316
+ <template data-ln-route="*" data-ln-route-target="side">
317
+ <section><p>Select a user to see details.</p></section>
318
+ </template>
319
+ ```
320
+
321
+ ### The primary region
322
+
323
+ The primary region is the default outlet (`[data-ln-outlet]` or first `<main>`). It is authoritative for:
324
+ - The single cancelable `ln-router:before-navigate` event (fires once per navigation)
325
+ - `document.title` updates
326
+ - Focus shifting and scroll management
327
+ - `ln-router:not-found` (when the primary has no match)
328
+ - `router.current()` return value
329
+
330
+ `data-ln-route-keep` is ignored on the primary region: the primary always tears down and re-renders on every matched navigation.
331
+
332
+ > [!IMPORTANT]
333
+ > **Each region matches the URL independently.** A route targeting an
334
+ > auxiliary region (`data-ln-route-target="…"`) no longer competes with primary
335
+ > routes for a single "best match" — every region runs its own match. If you
336
+ > have a route like `/users/:id/logs` that targets only an auxiliary region,
337
+ > the primary region will independently try to match `/users/42/logs` against
338
+ > its own templates and fall back to its `*` catch-all (or fire `not-found`) if
339
+ > it has no template for that path. Author a primary template for such paths if
340
+ > you want the main outlet to show meaningful content alongside the auxiliary
341
+ > region. This is a deliberate consequence of the parallel-regions model.
342
+
343
+ ### `data-ln-route-keep` — persistent regions
344
+
345
+ Add `data-ln-route-keep` to any target element to make it a **keep region**.
346
+
347
+ ```html
348
+ <aside id="side" data-ln-route-keep></aside>
349
+ ```
350
+
351
+ Keep-region logic:
352
+ - **Same template matches again** (e.g. param-only change `/users/42` → `/users/43`): the region is left completely untouched. No teardown, no DOM swap, no `ln-router:navigated` event. Input values, scroll position, and open panels survive.
353
+ - **Different template matches** (e.g. navigating to a route that has a different sidebar template): normal teardown + DOM swap + `ln-router:navigated` event.
354
+ - **No match for this URL**: the region is left as-is (untouched content persists). To clear a keep region on certain routes, declare an explicit empty `<template>` for those patterns.
355
+
356
+ Keep is a property of the **region target element**, not of the template.
357
+
358
+ ### `navigated` event — per swapped region
359
+
360
+ `ln-router:navigated` fires once per region that actually swapped. The `region` key identifies which region fired:
361
+
362
+ ```js
363
+ document.addEventListener('ln-router:navigated', function (e) {
364
+ const { path, params, query, route, target, region } = e.detail;
365
+
366
+ if (region === '__primary__') {
367
+ // Primary outlet navigated
368
+ } else if (region === 'side') {
369
+ // Sidebar region swapped
370
+ }
371
+ });
372
+ ```
373
+
374
+ The `region` value is `'__primary__'` for the default outlet, or the target element's `id` string for auxiliary regions.
375
+
376
+ **Backward compatibility:** all existing keys (`path, params, query, route, target`) remain in the event detail unchanged. Single-outlet pages continue to receive one `navigated` event with the same payload they always have.
377
+
378
+ ### `router.current()` — multi-region state
379
+
380
+ ```js
381
+ const state = router.current();
382
+ // {
383
+ // path: '/users/42',
384
+ // params: { id: '42' }, // primary region params
385
+ // query: {},
386
+ // route: { ... }, // primary region matched route
387
+ // regions: Map {
388
+ // '__primary__' => { route: {...}, params: { id: '42' } },
389
+ // 'side' => { route: {...}, params: { id: '42' } },
390
+ // }
391
+ // }
392
+ ```
393
+
394
+ ### Atomicity
395
+
396
+ All region swaps for a single navigation are wrapped in a single `document.startViewTransition` call. History push/replace happens once. `before-navigate` fires once.
397
+
398
+ ---
399
+
400
+ ## 🔧 Internals
401
+
402
+ Source: `js/ln-router/src/ln-router.js`.
403
+
404
+ ### Navigation pipeline (order matters)
405
+
406
+ 1. Strip hash/query, collapse trailing slashes.
407
+ 2. Every region in `regionRegistry` matches independently against its own sorted route list; the primary region's match is authoritative — if it has none (and no catch-all), the router dispatches `ln-router:not-found` and stops. Auxiliary-only pages proceed without firing `not-found`.
408
+ 3. `ln-router:before-navigate` fires once on the primary outlet; `preventDefault()` aborts the whole navigation before any region swap or history update.
409
+ 4. For each matched region, a keep-region check (`data-ln-route-keep` + same template already mounted) decides whether it enters the swap plan.
410
+ 5. History push/replace happens once.
411
+ 6. All planned swaps run sequentially inside a single `document.startViewTransition` call — teardown, clone, `replaceChildren`, record in `mountedTemplates`.
412
+ 7. `ln-router:navigated` fires per swapped region (`region` key = `'__primary__'` or the target id).
413
+
414
+ ### Registry & specificity
415
+
416
+ `regionRegistry: Map<regionKey, { routes: Map<pattern, meta>, sorted: meta[] }>` — one route table per region, each independently sorted static > param > wildcard, first-declared wins ties. A duplicate `(pattern, regionKey)` registration is dropped with `console.warn`; the same pattern targeting two different regions is legal.
417
+
418
+ ### Teardown & teleport cleanup
419
+
420
+ Only regions actually in the swap plan are torn down — a keep-region that skips a swap is never touched. Before `replaceChildren()` clears an outlet, the router walks its descendants for any element carrying an `ln*` property with a `destroy()` method and calls it — this is what prevents leaked window/document listeners and teleported overlay trees (e.g. an open popover in `<body>` whose trigger lived in the outlet) from surviving the swap.
421
+
422
+ ### Per-region persistence (keep regions)
423
+
424
+ A `WeakMap<Element, HTMLTemplateElement>` (`mountedTemplates`) tracks the currently-mounted template per target. A keep-region skips its swap only when the newly matched template node is the *same* node already mounted there — never for the primary region, which always re-renders.
425
+
426
+ ### SSR hydration
427
+
428
+ On boot, a region whose target carries `data-ln-router-hydrate` and already has children skips its initial clone swap — the SSR markup is left in place, listeners are wired, and `ln-router:navigated` still fires so a coordinator can bind/fill it.
429
+
430
+ ### Boot timing
431
+
432
+ A module-level `_booting` flag is true only during the router's own initial `_navigate` call inside `_boot()`. While true, `ln-router:navigated`/`not-found` dispatches are queued via `queueMicrotask` instead of firing synchronously — guaranteeing that listeners registered later in the same `defer`-script burst still receive the boot event. Every subsequent navigation dispatches synchronously.
@@ -0,0 +1 @@
1
+ var LnAshlarComponent=(function(b){"use strict";if(typeof window<"u"){const t=console.warn;console.warn=function(...e){typeof e[0]=="string"&&(e[0].startsWith("[ln-")||e[0].startsWith("[lnCore"))&&!(document.documentElement.hasAttribute("data-ln-debug")||document.body&&document.body.hasAttribute("data-ln-debug"))||t.apply(console,e)}}const _={};function Y(t,e){_[t]||(_[t]=document.querySelector('[data-ln-template="'+t+'"]'));const n=_[t];return n?n.content.cloneNode(!0):(console.warn("["+(e||"ln-core")+'] Template "'+t+'" not found'),null)}function R(t,e,n){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:n||{}}))}function Z(t,e,n){const s=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:n||{}});return t.dispatchEvent(s),s}function D(t,e){if(!t||!e)return t;const n=t.querySelectorAll("[data-ln-field]");for(let o=0;o<n.length;o++){const l=n[o],a=l.getAttribute("data-ln-field");e[a]!=null&&(l.textContent=e[a])}const s=t.querySelectorAll("[data-ln-attr]");for(let o=0;o<s.length;o++){const l=s[o],a=l.getAttribute("data-ln-attr").split(",");for(let c=0;c<a.length;c++){const d=a[c].trim().split(":");if(d.length!==2)continue;const u=d[0].trim(),f=d[1].trim();e[f]!=null&&l.setAttribute(u,e[f])}}const r=t.querySelectorAll("[data-ln-show]");for(let o=0;o<r.length;o++){const l=r[o],a=l.getAttribute("data-ln-show");a in e&&l.classList.toggle("hidden",!e[a])}const i=t.querySelectorAll("[data-ln-class]");for(let o=0;o<i.length;o++){const l=i[o],a=l.getAttribute("data-ln-class").split(",");for(let c=0;c<a.length;c++){const d=a[c].trim().split(":");if(d.length!==2)continue;const u=d[0].trim(),f=d[1].trim();f in e&&l.classList.toggle(u,!!e[f])}}return t}function tt(t,e){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));const n=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let s=0;s<n.length;s++)n[s].dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));return t}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore._fillBound||(window.lnCore._fillBound=!0,document.addEventListener("ln-fill",function(t){if(!(!t.target.matches||!t.target.matches("[data-ln-fillable]")))if(t.detail)D(t.target,t.detail);else{const e=t.target.querySelectorAll("[data-ln-field]");for(let n=0;n<e.length;n++)e[n].textContent=""}})));function W(t,e){if(!t||!e)return t;const n=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;n.nextNode();){const i=n.currentNode;i.textContent.indexOf("{{")!==-1&&(i.textContent=i.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(o,l){return e[l]!==void 0?e[l]:""}))}const s=function(i,o){return e[o]!==void 0?e[o]:""},r=Array.from(t.querySelectorAll("*"));t.nodeType===1&&r.push(t);for(let i=0;i<r.length;i++){const o=r[i],l=o.attributes;for(let a=0;a<l.length;a++){const c=l[a];c.value.indexOf("{{")!==-1&&o.setAttribute(c.name,c.value.replace(/\{\{\s*(\w+)\s*\}\}/g,s))}}return t}function et(t,e,n,s,r,i){const o={};for(let a=0;a<t.children.length;a++){const c=t.children[a],d=c.getAttribute("data-ln-key");d&&(o[d]=c)}const l=document.createDocumentFragment();for(let a=0;a<e.length;a++){const c=e[a],d=String(s(c));let u=o[d];if(u)r(u,c,a);else{const f=Y(n,i);if(!f||(W(f,c),u=f.firstElementChild,!u))continue;u.setAttribute("data-ln-key",d),r(u,c,a)}l.appendChild(u)}t.textContent="",t.appendChild(l)}function A(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){A(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function C(t,e,n,s){if(t.nodeType!==1)return;const i=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",o=Array.from(t.querySelectorAll(i));t.matches&&t.matches(i)&&o.push(t);for(const l of o)l[n]||(l[n]=new s(l))}function nt(t,e,n,s,r={}){const i=r.extraAttributes||[],o=r.onAttributeChange||null,l=r.onInit||null;function a(c){const d=c||document.body;C(d,t,e,n),l&&l(d)}return A(function(){const c=new MutationObserver(function(u){for(let f=0;f<u.length;f++){const h=u[f];if(h.type==="childList"){for(let p=0;p<h.addedNodes.length;p++){const m=h.addedNodes[p];m.nodeType===1&&(C(m,t,e,n),l&&l(m))}for(let p=0;p<h.removedNodes.length;p++){const m=h.removedNodes[p];if(m.nodeType===1){const X=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",q=Array.from(m.querySelectorAll(X));m.matches&&m.matches(X)&&q.push(m);for(let M=0;M<q.length;M++){const J=q[M];if(!document.contains(J)){const T=J[e];T&&typeof T.destroy=="function"&&T.destroy()}}}}}else h.type==="attributes"&&(o&&h.target[e]?o(h.target,h.attributeName):(C(h.target,t,e,n),l&&l(h.target)))}});let d=[];if(t.indexOf("[")!==-1){const u=/\[([\w-]+)/g;let f;for(;(f=u.exec(t))!==null;)d.push(f[1])}else d.push(t);c.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:d.concat(i)})},s),window[e]=a,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){a(document.body)}):a(document.body),a}function rt(t,e){if(t.ctrlKey||t.metaKey||t.shiftKey||t.altKey||t.button!==0||!e)return!1;const n=e.getAttribute("href");return!(!n||e.getAttribute("target")==="_blank"||e.hasAttribute("download")||n.startsWith("mailto:")||n.startsWith("tel:")||n==="#"||n.startsWith("#")||e.hostname&&e.hostname!==window.location.hostname)}const I={};function ot(t,e){I[t]=e}function it(t){return I[t]||{ingress:e=>e,egress:e=>e}}const N={};function j(t,e){if(!t||typeof e!="object")return;const n=t.toLowerCase().split("-")[0];N[n]=e}function st(t){if(!t)return null;const e=t.toLowerCase().split("-")[0];return N[e]||null}j("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=ot,window.lnCore.getDataMapper=it,window.lnCore.registerLocaleFallback=j,window.lnCore.getLocaleFallback=st,window.lnCore.fillTemplate=W,window.lnCore.fill=D,window.lnCore.lnFill=tt,window.lnCore.renderList=et);function K(t){return(t||"").replace(/^#/,"")}function S(t){const e=t===void 0?location.hash:t,n={},s=K(e);if(!s)return n;const r=s.split("&");for(let i=0;i<r.length;i++){const o=r[i];if(!o)continue;const l=o.indexOf(":"),a=l>-1?o.slice(0,l):o,c=l>-1?o.slice(l+1):"";if(a)try{n[a]=decodeURIComponent(c)}catch{n[a]=c}}return n}function lt(t){if(!t)return null;const e=S();return t in e?e[t]:null}function at(t,e){if(!t)return;const n=S();e==null?delete n[t]:n[t]=String(e);const r=Object.keys(n).map(function(i){const o=n[i];return o===""?i:i+":"+encodeURIComponent(o)}).join("&");K(location.hash)!==r&&(location.hash=r)}function ct(t){return t.button===1||t.ctrlKey||t.metaKey||t.shiftKey?!1:(t.preventDefault(),!0)}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.hashParse=S,window.lnCore.hashGet=lt,window.lnCore.hashSet=at,window.lnCore.hashLinkClick=ct);const v={navigate:function(t){y(t,{historyAction:"push"})},replace:function(t){y(t,{historyAction:"replace"})},current:function(){return E?{path:x,params:F,query:U,route:E,regions:H}:null}},k="data-ln-route",P="lnRoute";typeof window<"u"&&(window.lnRouter=v);const g=new Map,B=new WeakMap;let H=new Map,$=!1,x=null,F={},U={},E=null,L=!1;function z(t,e,n){L?queueMicrotask(function(){R(t,e,n)}):R(t,e,n)}function w(t){try{const i=new URL(t,window.location.origin);t=i.pathname+i.search+i.hash}catch{}let[e]=t.split("#"),[n,s]=e.split("?");const r={};if(s){const i=new URLSearchParams(s);for(const[o,l]of i.entries())r[o]=l}return n=n.replace(/\/+$/,""),n===""&&(n="/"),{path:n,query:r}}function V(t,e){if(t.pattern==="*")return 1;if(e.pattern==="*")return-1;const n=t.segments,s=e.segments,r=Math.max(n.length,s.length);for(let i=0;i<r;i++){const o=n[i],l=s[i];if(o===void 0)return 1;if(l===void 0)return-1;if(o==="*")return 1;if(l==="*")return-1;const a=o.startsWith(":"),c=l.startsWith(":");if(a&&!c)return 1;if(!a&&c)return-1}return 0}function G(t,e){const n=t.split("/").filter(Boolean);for(const s of e){if(s.pattern==="*")return{route:s,params:{wildcard:t}};const r=s.segments,i={};let o=!0;if(!(n.length>r.length&&r[r.length-1]!=="*")){for(let l=0;l<r.length;l++){const a=r[l],c=n[l];if(a==="*"){i.wildcard=n.slice(l).join("/");break}if(c===void 0){o=!1;break}if(a.startsWith(":"))i[a.slice(1)]=decodeURIComponent(c);else if(a!==c){o=!1;break}}if(o&&(r.indexOf("*")!==-1||n.length<=r.length))return{route:s,params:i}}}return null}function O(t,e){if(t!=="__primary__"){const s=document.getElementById(e.target);return s||console.warn(`[ln-router] Explicit target element #${e.target} not found in DOM`),s}const n=document.querySelector("[data-ln-outlet]")||document.querySelector("main");return n||console.warn("[ln-router] Default outlet (element with [data-ln-outlet] or <main>) not found in DOM"),n}function ut(t){if(!t)return;const e=Array.from(t.querySelectorAll("*")),n=[t].concat(e);for(const r of n)for(const i of Object.keys(r))if(i.startsWith("ln")&&r[i]&&typeof r[i].destroy=="function")try{r[i].destroy()}catch(o){console.error(`[ln-router] Error destroying component ${i} on element:`,r,o)}const s=document.querySelectorAll('[data-ln-popover="open"]');for(const r of s){const i=r.lnPopover;if(i&&i.trigger&&t.contains(i.trigger))try{i.destroy()}catch(o){console.error("[ln-router] Error destroying open popover:",o)}}}function y(t,e={}){const{path:n,query:s}=w(t),r=new Map;for(const[d,u]of g)r.set(d,G(n,u.sorted));const i=g.has("__primary__"),o=r.get("__primary__");if(i&&!o){z(document.body,"ln-router:not-found",{path:n});return}let l=null;if(o&&(l=O("__primary__",o.route),!l||Z(l,"ln-router:before-navigate",{from:x,to:t,params:o.params,query:s}).defaultPrevented))return;const a=[];for(const[d,u]of r){if(!u)continue;const f=O(d,u.route);f&&(d!=="__primary__"&&f.hasAttribute("data-ln-route-keep")&&B.get(f)===u.route.templateNode||a.push({regionKey:d,match:u,targetEl:f}))}e.historyAction==="push"?window.history.pushState(null,"",t):e.historyAction==="replace"&&window.history.replaceState(null,"",t);const c=function(){for(const{regionKey:d,match:u,targetEl:f}of a){if(!(e.isHydration&&f.hasAttribute("data-ln-router-hydrate")&&f.children.length>0)){ut(f);const p=u.route.templateNode.content.cloneNode(!0);f.replaceChildren(p)}if(B.set(f,u.route.templateNode),d==="__primary__"&&(u.route.title&&(document.title=u.route.title),!e.isHydration)){f.hasAttribute("tabindex")||f.setAttribute("tabindex","-1");const p=f.querySelector("h1, h2, h3, h4, h5, h6");p?(p.setAttribute("tabindex","-1"),p.focus()):f.focus(),f.scrollIntoView({block:"start",behavior:"instant"})}z(f,"ln-router:navigated",{path:t,params:u.params,query:s,route:u.route,target:f,region:d})}o&&(x=t,F=o.params,U=s,E=o.route),H=new Map(Array.from(r.entries()).map(([d,u])=>[d,u?{route:u.route,params:u.params}:null]))};document.startViewTransition&&!e.isHydration?document.startViewTransition(c):c()}function ft(t){const e=t.target.closest("a");if(!e||!rt(t,e))return;const n=e.getAttribute("href"),{path:s}=w(n),r=g.get("__primary__");if(!r)return;G(s,r.sorted)&&(t.preventDefault(),y(n,{historyAction:"push"}))}function dt(t,e){const n=Object.keys(t),s=Object.keys(e);if(n.length!==s.length)return!1;for(let r=0;r<n.length;r++){const i=n[r];if(t[i]!==e[i])return!1}return!0}function ht(){const t=window.location.pathname+window.location.search,e=v.current();if(e&&e.path!=null){const n=w(t);if(w(e.path).path===n.path&&dt(e.query,n.query))return}y(t,{historyAction:"skip"})}function pt(){$||($=!0,A(function(){document.addEventListener("click",ft),window.addEventListener("popstate",ht),L=!0;const t=window.location.pathname+window.location.search+window.location.hash;y(t,{historyAction:"replace",isHydration:!0}),L=!1},"ln-router"))}function gt(t){const e=t.getAttribute(k);if(!e)return;const n=t.getAttribute("data-ln-route-target")||null;if(n==="__primary__"){console.warn(`[ln-router] "__primary__" is a reserved region key and cannot be used as data-ln-route-target. Route "${e}" rejected.`);return}const s=n||"__primary__";g.has(s)||g.set(s,{routes:new Map,sorted:[]});const r=g.get(s);if(r.routes.has(e)){console.warn(`[ln-router] Duplicate route pattern registered: "${e}" in region "${s}"`);return}const i=t.getAttribute("data-ln-route-title"),o=e.split("/").filter(Boolean),l={pattern:e,segments:o,target:n,title:i,templateNode:t},a=O(s,l);a&&a.contains(t)&&console.warn(`[ln-router] Route template with pattern "${e}" is declared inside its own outlet element:`,t),r.routes.set(e,l),r.sorted=Array.from(r.routes.values()).sort(V)}function mt(t){const e=t.getAttribute(k);if(!e)return;const s=t.getAttribute("data-ln-route-target")||null||"__primary__",r=g.get(s);r&&(r.routes.delete(e),r.sorted=Array.from(r.routes.values()).sort(V),r.routes.size===0&&g.delete(s))}function Q(t){return this.dom=t,gt(t),this}return Q.prototype.destroy=function(){mt(this.dom),delete this.dom[P]},nt(k,P,Q,"ln-router",{extraAttributes:["data-ln-route-target","data-ln-route-title"],onInit:function(){g.size>0&&pt()}}),b.router=v,Object.defineProperty(b,Symbol.toStringTag,{value:"Module"}),b})({});