@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.
- package/README.md +189 -177
- package/js/COMPONENTS.md +55 -49
- package/js/index.js +22 -5
- package/js/ln-accordion/README.md +186 -137
- package/js/ln-accordion/ln-accordion.js +1 -1
- package/js/ln-ajax/README.md +73 -4
- package/js/ln-ajax/ln-ajax.js +1 -1
- package/js/ln-ajax/src/ln-ajax.js +13 -13
- package/js/ln-api-connector/README.md +261 -150
- package/js/ln-api-connector/ln-api-connector.js +1 -1
- package/js/ln-api-connector/src/ln-api-connector.js +368 -265
- package/js/ln-api-queue/README.md +219 -0
- package/js/ln-api-queue/ln-api-queue.js +1 -0
- package/js/ln-api-queue/src/ln-api-queue.js +320 -0
- package/js/ln-api-queue/src/queue-storage.js +455 -0
- package/js/ln-autoresize/README.md +47 -0
- package/js/ln-autoresize/ln-autoresize.js +1 -1
- package/js/ln-autosave/README.md +33 -1
- package/js/ln-autosave/ln-autosave.js +1 -1
- package/js/ln-autosave/src/ln-autosave.js +4 -4
- package/js/ln-circular-progress/README.md +27 -13
- package/js/ln-circular-progress/ln-circular-progress.js +1 -1
- package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
- package/js/ln-confirm/README.md +162 -86
- package/js/ln-confirm/_ln-confirm.scss +11 -0
- package/js/ln-confirm/ln-confirm.js +1 -1
- package/js/ln-confirm/src/ln-confirm.js +77 -41
- package/js/ln-core/README.md +896 -0
- package/js/ln-core/hash.js +92 -0
- package/js/ln-core/helpers.js +337 -8
- package/js/ln-core/index.js +4 -2
- package/js/ln-core/persist.js +29 -3
- package/js/ln-core/positioning.js +1 -35
- package/js/ln-core/window-cache.js +213 -0
- package/js/ln-couchdb-connector/README.md +177 -156
- package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
- package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
- package/js/ln-data-coordinator/README.md +536 -165
- package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
- package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
- package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
- package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
- package/js/ln-data-store/README.md +241 -94
- package/js/ln-data-store/ln-data-store.js +1 -1
- package/js/ln-data-store/src/ln-data-store.js +694 -699
- package/js/ln-date/README.md +39 -1
- package/js/ln-date/ln-date-dev.scss +14 -0
- package/js/ln-date/ln-date.js +1 -1
- package/js/ln-date/src/ln-date.js +236 -178
- package/js/ln-debug/README.md +58 -0
- package/js/ln-debug/ln-debug.js +1 -0
- package/js/ln-debug/src/ln-debug.js +19 -0
- package/js/ln-dropdown/README.md +31 -8
- package/js/ln-dropdown/ln-dropdown.js +1 -1
- package/js/ln-dropdown/src/ln-dropdown.js +30 -13
- package/js/ln-editor/README.md +274 -0
- package/js/ln-editor/ln-editor.js +1 -0
- package/js/ln-editor/ln-editor.scss +43 -0
- package/js/ln-editor/src/ln-editor.js +619 -0
- package/js/ln-external-links/README.md +44 -5
- package/js/ln-external-links/ln-external-links.js +1 -1
- package/js/ln-fill/README.md +199 -0
- package/js/ln-fill/ln-fill.js +1 -0
- package/js/ln-fill/src/ln-fill.js +87 -0
- package/js/ln-filter/README.md +92 -2
- package/js/ln-filter/ln-filter-dev.scss +8 -0
- package/js/ln-filter/ln-filter.js +1 -1
- package/js/ln-filter/src/ln-filter.js +22 -7
- package/js/ln-form/README.md +212 -49
- package/js/ln-form/ln-form.js +1 -1
- package/js/ln-form/src/ln-form.js +99 -199
- package/js/ln-http/README.md +125 -89
- package/js/ln-http/ln-http.js +1 -1
- package/js/ln-http/src/ln-http.js +32 -13
- package/js/ln-icon/README.md +130 -0
- package/js/ln-icon/ln-icon.js +1 -0
- package/js/{ln-icons/src/ln-icons.js → ln-icon/src/ln-icon.js} +20 -15
- package/js/ln-link/README.md +47 -9
- package/js/ln-link/ln-link.js +1 -1
- package/js/ln-link/src/ln-link.js +22 -6
- package/js/ln-list/README.md +129 -0
- package/js/ln-list/ln-list.js +1 -0
- package/js/ln-list/ln-list.scss +28 -0
- package/js/ln-list/src/ln-list.js +1216 -0
- package/js/ln-modal/README.md +185 -154
- package/js/ln-modal/ln-modal-dev.scss +13 -0
- package/js/ln-modal/ln-modal.js +1 -1
- package/js/ln-modal/ln-modal.scss +28 -4
- package/js/ln-modal/src/ln-modal.js +137 -201
- package/js/ln-modal-coordinator/README.md +99 -0
- package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +8 -0
- package/js/ln-modal-coordinator/ln-modal-coordinator.js +1 -0
- package/js/ln-modal-coordinator/src/ln-modal-coordinator.js +345 -0
- package/js/ln-nav/README.md +46 -3
- package/js/ln-nav/ln-nav.js +1 -1
- package/js/ln-nav/src/ln-nav.js +92 -127
- package/js/ln-number/README.md +28 -1
- package/js/ln-number/ln-number-dev.scss +13 -0
- package/js/ln-number/ln-number.js +1 -1
- package/js/ln-number/src/ln-number.js +443 -350
- package/js/ln-options/README.md +66 -0
- package/js/ln-options/ln-options.js +1 -0
- package/js/ln-options/src/ln-options.js +74 -0
- package/js/ln-popover/README.md +40 -5
- package/js/ln-popover/ln-popover.js +1 -1
- package/js/ln-popover/src/ln-popover.js +38 -11
- package/js/ln-progress/README.md +413 -442
- package/js/ln-progress/ln-progress.js +1 -1
- package/js/ln-progress/src/ln-progress.js +98 -150
- package/js/ln-router/README.md +432 -0
- package/js/ln-router/ln-router.js +1 -0
- package/js/ln-router/src/ln-router.js +539 -0
- package/js/ln-search/README.md +158 -83
- package/js/ln-search/ln-search-dev.scss +8 -0
- package/js/ln-search/ln-search.js +1 -1
- package/js/ln-search/src/ln-search.js +12 -9
- package/js/ln-slug/README.md +110 -0
- package/js/ln-slug/ln-slug.js +1 -0
- package/js/ln-slug/src/ln-slug.js +74 -0
- package/js/ln-sort/README.md +170 -0
- package/js/ln-sort/ln-sort-dev.scss +8 -0
- package/js/ln-sort/ln-sort.js +1 -0
- package/js/ln-sort/src/ln-sort.js +178 -0
- package/js/ln-sortable/README.md +133 -95
- package/js/ln-sortable/ln-sortable.js +1 -1
- package/js/ln-stat/README.md +55 -0
- package/js/ln-stat/ln-stat.js +1 -0
- package/js/ln-stat/src/ln-stat.js +55 -0
- package/js/ln-table/README.md +285 -101
- package/js/ln-table/ln-table-dev.scss +17 -0
- package/js/ln-table/ln-table.js +1 -1
- package/js/ln-table/ln-table.scss +1 -1
- package/js/ln-table/src/ln-table.js +1493 -473
- package/js/ln-table-coordinator/README.md +73 -0
- package/js/ln-table-coordinator/ln-table-coordinator.js +1 -0
- package/js/ln-table-coordinator/src/ln-table-coordinator.js +160 -0
- package/js/ln-tabs/README.md +221 -137
- package/js/ln-tabs/ln-tabs-dev.scss +18 -0
- package/js/ln-tabs/ln-tabs.js +1 -1
- package/js/ln-tabs/src/ln-tabs.js +41 -24
- package/js/ln-time/README.md +14 -0
- package/js/ln-time/ln-time.js +1 -1
- package/js/ln-time/src/ln-time.js +223 -192
- package/js/ln-toast/README.md +178 -122
- package/js/ln-toast/ln-toast.js +1 -15
- package/js/ln-toast/src/ln-toast.js +232 -210
- package/js/ln-toast/template.html +16 -12
- package/js/ln-toggle/README.md +61 -2
- package/js/ln-toggle/ln-toggle-dev.scss +13 -0
- package/js/ln-toggle/ln-toggle.js +1 -1
- package/js/ln-toggle/src/ln-toggle.js +56 -43
- package/js/ln-tooltip/README.md +95 -58
- package/js/ln-tooltip/ln-tooltip-dev.scss +9 -0
- package/js/ln-tooltip/ln-tooltip.js +1 -1
- package/js/ln-tooltip/src/ln-tooltip.js +45 -6
- package/js/ln-translations/README.md +118 -96
- package/js/ln-translations/ln-translations.js +1 -1
- package/js/ln-translations/src/ln-translations.js +10 -5
- package/js/ln-upload/README.md +85 -10
- package/js/ln-upload/ln-upload.js +1 -1
- package/js/ln-upload/src/ln-upload.js +26 -10
- package/js/ln-validate/README.md +72 -0
- package/js/ln-validate/ln-validate-dev.scss +15 -0
- package/js/ln-validate/ln-validate.js +1 -1
- package/js/ln-validate/src/ln-validate.js +64 -0
- package/package.json +61 -56
- package/scss/base/_global.scss +2 -2
- package/scss/base/_typography.scss +1 -2
- package/scss/components/_date.scss +1 -0
- package/scss/components/_dropdown.scss +0 -5
- package/scss/components/_editor.scss +5 -0
- package/scss/components/_filter.scss +26 -25
- package/scss/components/_form.scss +7 -0
- package/scss/components/_ln-table.scss +13 -3
- package/scss/components/_sort.scss +10 -0
- package/scss/components/_table.scss +36 -23
- package/scss/components/_toast.scss +68 -41
- package/scss/components/_toggle.scss +16 -0
- package/scss/config/_density.scss +205 -117
- package/scss/config/_icons.scss +10 -9
- package/scss/config/_tokens.scss +69 -47
- package/scss/config/mixins/_accordion.scss +2 -7
- package/scss/config/mixins/_alert.scss +4 -4
- package/scss/config/mixins/_breadcrumbs.scss +2 -2
- package/scss/config/mixins/_btn.scss +1 -1
- package/scss/config/mixins/_card.scss +89 -14
- package/scss/config/mixins/_collapsible.scss +3 -0
- package/scss/config/mixins/_colors.scss +1 -1
- package/scss/config/mixins/_diagnostics.scss +11 -0
- package/scss/config/mixins/_display.scss +1 -0
- package/scss/config/mixins/_dropdown.scss +3 -5
- package/scss/config/mixins/_editor.scss +124 -0
- package/scss/config/mixins/_empty-state.scss +7 -0
- package/scss/config/mixins/_filter.scss +3 -5
- package/scss/config/mixins/_footer.scss +1 -2
- package/scss/config/mixins/_form.scss +207 -21
- package/scss/config/mixins/_index.scss +2 -0
- package/scss/config/mixins/_ln-table.scss +41 -2
- package/scss/config/mixins/_modal.scss +149 -66
- package/scss/config/mixins/_nav.scss +3 -4
- package/scss/config/mixins/_popover.scss +39 -0
- package/scss/config/mixins/_sidebar.scss +1 -1
- package/scss/config/mixins/_sort.scss +42 -0
- package/scss/config/mixins/_table.scss +85 -60
- package/scss/config/mixins/_toast.scss +179 -152
- package/scss/config/mixins/_tooltip.scss +17 -7
- package/scss/config/mixins/_typography.scss +4 -2
- package/scss/ln-ashlar-dev.scss +27 -9
- package/scss/ln-ashlar.scss +7 -1
- package/js/ln-data-table/README.md +0 -110
- package/js/ln-data-table/ln-data-table.js +0 -1
- package/js/ln-data-table/ln-data-table.scss +0 -10
- package/js/ln-data-table/src/ln-data-table.js +0 -1087
- package/js/ln-icons/README.md +0 -88
- package/js/ln-icons/ln-icons.js +0 -1
- package/js/ln-table/ln-table-sort.js +0 -1
- 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})({});
|