@livenetworks/ashlar 1.5.1 → 1.6.1

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 +165 -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 +82 -42
  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
@@ -1,150 +1,261 @@
1
- # `data-ln-api-connector`
2
-
3
- A zero-dependency, Local-First sync transport component that implements the Transport Gateway pattern of `ln-ashlar`.
4
-
5
- This component encapsulates all connection parameters (base URLs, auth tokens, headers, paths) and provides a declarative, event-driven, or programmatic way to talk to any RESTful or JSON API backend. It isolates networking concerns completely, making your cache store (`data-ln-data-store`) and visual presentation layers fully network-agnostic.
6
-
7
- ---
8
-
9
- ## Declarative DOM Setup
10
-
11
- Place the connector inside your parent coordinator element alongside your store:
12
-
13
- ```html
14
- <div data-ln-data-coordinator="documents">
15
- <!-- Storage Layer Cache (Blind to networking) -->
16
- <div data-ln-data-store
17
- data-ln-store-indexes="status,updated_at">
18
- </div>
19
-
20
- <!-- Transport Gateway (REST / API Connector) -->
21
- <div data-ln-api-connector
22
- data-ln-api-base-url="https://api.livenetworks.com/v1"
23
- data-ln-api-path="/documents"
24
- data-ln-api-headers='{"Authorization": "Bearer tok_123"}'>
25
- </div>
26
- </div>
27
- ```
28
-
29
- ---
30
-
31
- ## Attributes
32
-
33
- All attributes are dynamically observed. Any runtime changes to these attributes instantly update the connector's internal configurations.
34
-
35
- | Attribute | Category | Description |
36
- |-----------|----------|-------------|
37
- | `data-ln-api-connector` | Selector | Creates the component instance. Optional value serves as name. |
38
- | `data-ln-api-base-url` | Connection | The base URL of the API gateway (e.g. `https://api.example.com/v1` or `/api`). Fallback: `data-ln-api-connector-base-url`, `data-ln-rest-base-url`. |
39
- | `data-ln-api-path` | Connection | Resource path endpoint (e.g. `/documents`). Fallback: `data-ln-api-connector-path`, `data-ln-rest-path`. |
40
- | `data-ln-api-headers` | Credentials | JSON-formatted string of request headers (e.g. `{"Authorization": "Bearer tok_123"}`). Fallback: `data-ln-api-connector-headers`, `data-ln-rest-headers`. |
41
-
42
- ---
43
-
44
- ## JavaScript API Methods
45
-
46
- You can access the instance methods directly on the element via the `.lnApiConnector` or `.lnConnector` properties:
47
-
48
- ```javascript
49
- const connectorEl = document.querySelector('[data-ln-api-connector]');
50
- const connector = connectorEl.lnApiConnector; // or connectorEl.lnConnector
51
-
52
- // 1. Fetch changed records since a Unix timestamp (delta protocol)
53
- connector.fetchDelta(1736952600)
54
- .then(data => console.log('Sync arrays loaded:', data));
55
-
56
- // 2. Create a new record
57
- connector.create({ title: 'New Document', status: 'Draft' })
58
- .then(record => console.log('Created record:', record));
59
-
60
- // 3. Update an existing record
61
- connector.update(42, { title: 'Updated Title' })
62
- .then(record => console.log('Updated record:', record));
63
-
64
- // 4. Delete a record
65
- connector.delete(42)
66
- .then(res => console.log('Deleted:', res));
67
-
68
- // 5. Bulk Delete records
69
- connector.bulkDelete([17, 23])
70
- .then(res => console.log('Bulk deleted:', res));
71
- ```
72
-
73
- ---
74
-
75
- ## DOM Events
76
-
77
- ### Commands (Dispatched TO the connector)
78
-
79
- You can trigger mutations and fetches asynchronously by dispatching standard events directly on the connector DOM element. All events are supported in both `ln-api-connector` and legacy `ln-rest-connector` namespaces.
80
-
81
- | Event | `detail` Payload | Description |
82
- |-------|------------------|-------------|
83
- | `ln-api-connector:request-sync` | `{ since }` | Triggers a delta fetch request. |
84
- | `ln-api-connector:request-create` | `{ data, tempId }` | Triggers a creation request. |
85
- | `ln-api-connector:request-update` | `{ id, data, expected_version }` | Triggers a PUT update (supports 409 conflict checks). |
86
- | `ln-api-connector:request-delete` | `{ id }` | Triggers a deletion request. |
87
- | `ln-api-connector:request-bulk-delete` | `{ ids }` | Triggers a bulk-deletion request. |
88
-
89
- ### Notifications (Emitted BY the connector)
90
-
91
- The connector dispatches bubbles-enabled events to notify parent coordinators of response states.
92
-
93
- | Event | `detail` Payload | Description |
94
- |-------|------------------|-------------|
95
- | `ln-api-connector:fetched` | `{ data, since }` | Dispatched upon successful fetch completion. |
96
- | `ln-api-connector:created` | `{ record, tempId }` | Dispatched upon successful server record confirmation. |
97
- | `ln-api-connector:updated` | `{ record, id }` | Dispatched upon successful server update confirmation. |
98
- | `ln-api-connector:deleted` | `{ response, id }` | Dispatched upon successful server deletion. |
99
- | `ln-api-connector:bulk-deleted`| `{ response, ids }` | Dispatched upon successful server bulk deletion. |
100
- | `ln-api-connector:error` | `{ action, error, status, ... }` | Dispatched on failures. Includes `conflictData` if status is 409. |
101
- | `ln-api-connector:config-changed` | `{ baseUrl, path, headers }` | Dispatched when configuration attributes are mutated. |
102
-
103
- ---
104
-
105
- ## 3-Tier Integration Architecture (Coordinator Example)
106
-
107
- Here is how a parent Coordinator links a Cache Store and the API Connector using standard DOM events:
108
-
109
- ```javascript
110
- (function () {
111
- const parent = document.querySelector('[data-ln-data-coordinator="documents"]');
112
- const storeEl = parent.querySelector('[data-ln-data-store]');
113
- const connectorEl = parent.querySelector('[data-ln-api-connector]');
114
-
115
- if (!storeEl || !connectorEl) return;
116
-
117
- // 1. Storage needs remote sync -> forward request to Connector
118
- storeEl.addEventListener('ln-store:request-remote-sync', function (e) {
119
- connectorEl.dispatchEvent(new CustomEvent('ln-api-connector:request-sync', {
120
- detail: { since: e.detail.since }
121
- }));
122
- });
123
-
124
- // 2. Connector finishes sync -> feed delta back to Storage
125
- connectorEl.addEventListener('ln-api-connector:fetched', function (e) {
126
- const payload = e.detail.data;
127
- storeEl.lnStore.applySync(payload.data, payload.deleted || [], payload.synced_at);
128
- });
129
-
130
- // 3. Storage requests remote creation -> forward payload to Connector
131
- storeEl.addEventListener('ln-store:request-remote-create', function (e) {
132
- connectorEl.dispatchEvent(new CustomEvent('ln-api-connector:request-create', {
133
- detail: { data: e.detail.data, tempId: e.detail.tempId }
134
- }));
135
- });
136
-
137
- // 4. Connector confirms creation -> swap optimistic temp records in Storage
138
- connectorEl.addEventListener('ln-api-connector:created', function (e) {
139
- storeEl.lnStore.confirmMutation(e.detail.tempId, e.detail.record, 'create');
140
- });
141
-
142
- // 5. In case of network errors -> trigger rollback in Cache Store
143
- connectorEl.addEventListener('ln-api-connector:error', function (e) {
144
- if (e.detail.action === 'create') {
145
- storeEl.lnStore.revertMutation(e.detail.tempId, 'create', e.detail.error);
146
- }
147
- // ... handle updates and deletions similarly
148
- });
149
- })();
150
- ```
1
+ # `data-ln-api-connector`
2
+
3
+ A zero-dependency, Local-First sync transport component that implements the Transport Gateway pattern of `ln-ashlar`.
4
+
5
+ This component encapsulates all connection parameters (base URLs, auth tokens, headers, paths) and provides a declarative, event-driven, or programmatic way to talk to any RESTful or JSON API backend. It isolates networking concerns completely, making your cache store (`data-ln-data-store`) and visual presentation layers fully network-agnostic.
6
+
7
+ ---
8
+
9
+ ## 🔒 Forced `X-LN-Response: data` Header
10
+
11
+ Every request (`fetchDelta`, `create`, `update`, `delete`, `bulkDelete`) sends
12
+ `X-LN-Response: data` in addition to any configured `data-ln-api-headers`.
13
+ This header is **forced** — it rides on all five verbs via a single internal
14
+ helper and cannot be removed or overridden by consumer-supplied headers. The
15
+ backend uses it to select a JSON "data" response mode; it doubles as a CSRF
16
+ guard.
17
+
18
+ Queued mutation commands may also provide `idempotencyKey`. The connector
19
+ forwards it as `Idempotency-Key`; direct/non-queued calls omit it. Servers
20
+ should persist the first result for a key so lease recovery can safely retry a
21
+ request after a client crash.
22
+
23
+ ## ⚠️ Unified 4xx / 5xx Body Parsing
24
+
25
+ Every non-2xx response (not just 409) has its JSON body parsed best-effort
26
+ and attached to the rejected error as `err.data` (`null` if the body isn't
27
+ parseable JSON), alongside `err.status`. This means server validation
28
+ messages (422, etc.) survive on every verb, not just conflict responses.
29
+
30
+ ## `update(id, payload, expectedVersion, url)` — optional third/fourth argument
31
+
32
+ `update` accepts an optional third argument. When provided (not `null`/
33
+ `undefined`), the connector merges it into the outgoing PUT body as
34
+ `expected_version`: `payload = Object.assign({}, payload, { expected_version: expectedVersion })`.
35
+ This is a single merge point — the `ln-api-connector:request-update` event
36
+ path passes `detail.expected_version` straight through to it, so there is
37
+ no second place version-locking logic lives. The optional fourth argument,
38
+ `url`, when present, is treated as the COMPLETE resource URL for that call
39
+ — it already carries the record id (e.g. a form's resolved `action`
40
+ `/documents/42`), so `update()` does NOT append the id again (symmetric
41
+ with `create()`). Only the `path` fallback (no `url` given) appends the id.
42
+
43
+ ```javascript
44
+ // Programmatic call with version lock
45
+ connector.update(42, { title: 'Updated title' }, 3)
46
+ .then(record => console.log('Updated record:', record));
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Declarative DOM Setup
52
+
53
+ Place the connector inside your parent coordinator element alongside your store:
54
+
55
+ ```html
56
+ <ul data-ln-data-coordinator="documents" hidden>
57
+ <!-- Storage Layer Cache (Blind to networking) -->
58
+ <li data-ln-data-store
59
+ data-ln-data-store-indexes="status,updated_at">
60
+ </li>
61
+
62
+ <!-- Transport Gateway (REST / API Connector) -->
63
+ <li data-ln-api-connector
64
+ data-ln-api-base-url="https://api.livenetworks.com/v1"
65
+ data-ln-api-path="/documents"
66
+ data-ln-api-headers='{"Authorization": "Bearer tok_123"}'>
67
+ </li>
68
+ </ul>
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Attributes
74
+
75
+ All attributes are dynamically observed. Any runtime changes to these attributes instantly update the connector's internal configurations.
76
+
77
+ | Attribute | Category | Description |
78
+ |-----------|----------|-------------|
79
+ | `data-ln-api-connector` | Selector | Creates the component instance. Optional value serves as name. |
80
+ | `data-ln-api-base-url` | Connection | The base URL of the API gateway (e.g. `https://api.example.com/v1` or `/api`). Fallback: `data-ln-api-connector-base-url`, `data-ln-rest-base-url`. |
81
+ | `data-ln-api-path` | Connection | Resource path endpoint (e.g. `/documents`). Fallback: `data-ln-api-connector-path`, `data-ln-rest-path`. |
82
+ | `data-ln-api-headers` | Credentials | JSON-formatted string of request headers (e.g. `{"Authorization": "Bearer tok_123"}`). Fallback: `data-ln-api-connector-headers`, `data-ln-rest-headers`. |
83
+
84
+ ---
85
+
86
+ ## JavaScript API Methods
87
+
88
+ You can access the instance methods directly on the element via the `.lnApiConnector` or `.lnConnector` properties:
89
+
90
+ ```javascript
91
+ const connectorEl = document.querySelector('[data-ln-api-connector]');
92
+ const connector = connectorEl.lnApiConnector; // or connectorEl.lnConnector
93
+
94
+ // 1. Fetch changed records since a Unix timestamp (delta protocol)
95
+ connector.fetchDelta(1736952600)
96
+ .then(data => console.log('Sync arrays loaded:', data));
97
+
98
+ // 2. Create a new record
99
+ connector.create({ title: 'New Document', status: 'Draft' })
100
+ .then(record => console.log('Created record:', record));
101
+
102
+ // 3. Update an existing record
103
+ connector.update(42, { title: 'Updated Title' })
104
+ .then(record => console.log('Updated record:', record));
105
+
106
+ // 4. Delete a record
107
+ connector.delete(42)
108
+ .then(res => console.log('Deleted:', res));
109
+
110
+ // 5. Bulk Delete records
111
+ connector.bulkDelete([17, 23])
112
+ .then(res => console.log('Bulk deleted:', res));
113
+ ```
114
+
115
+ ---
116
+
117
+ ## DOM Events
118
+
119
+ ### Commands (Dispatched TO the connector)
120
+
121
+ You can trigger mutations and fetches asynchronously by dispatching standard events directly on the connector DOM element. All events are supported in both `ln-api-connector` and legacy `ln-rest-connector` namespaces.
122
+
123
+ | Event | `detail` Payload | Description |
124
+ |-------|------------------|-------------|
125
+ | `ln-api-connector:request-sync` | `{ since, meta }` | Triggers a delta fetch request. |
126
+ | `ln-api-connector:request-create` | `{ data, tempId, url, idempotencyKey?, meta }` | Triggers a creation request. |
127
+ | `ln-api-connector:request-update` | `{ id, data, expected_version, url, idempotencyKey?, meta }` | Triggers a PUT update (supports 409 conflict checks). |
128
+ | `ln-api-connector:request-delete` | `{ id, url, idempotencyKey?, meta }` | Triggers a deletion request. |
129
+ | `ln-api-connector:request-bulk-delete` | `{ ids, url, idempotencyKey?, meta }` | Triggers a bulk-deletion request. |
130
+
131
+ `url` is optional — when present, it replaces the connector's own `path`
132
+ for that call (still joined with `data-ln-api-base-url`). For `update`,
133
+ `delete`, and `bulk-delete`, a supplied `url` is the complete resource URL
134
+ already carrying the id — the connector does not append the id a second
135
+ time (create has no id to append; delete/bulk-delete currently receive no
136
+ url in practice). `meta` is
137
+ opaque, default `null`, echoed untouched on the response.
138
+
139
+ `url` is same-origin-relative by convention (it originates from a form's
140
+ HTML `action` attribute) and always passes through the same `buildUrl()`
141
+ join as `path` — `data-ln-api-base-url` still prepends when configured.
142
+
143
+ ### Notifications (Emitted BY the connector)
144
+
145
+ The connector dispatches bubbles-enabled events to notify parent coordinators of response states.
146
+
147
+ | Event | `detail` Payload | Description |
148
+ |-------|------------------|-------------|
149
+ | `ln-api-connector:fetched` | `{ data, since, meta }` | Dispatched upon successful fetch completion. Echoes `detail.meta` from the triggering request untouched, `null` if not provided. |
150
+ | `ln-api-connector:created` | `{ record, tempId, message, meta }` | Dispatched upon successful server record confirmation. `record` is the unwrapped `content` (or the bare body — see envelope below). `message` is opaque, `null` if absent. Echoes `detail.meta` untouched, `null` if not provided. |
151
+ | `ln-api-connector:updated` | `{ record, id, message, meta }` | Dispatched upon successful server update confirmation. Same `record`/`message` unwrap as `:created`. Echoes `detail.meta` untouched, `null` if not provided. |
152
+ | `ln-api-connector:deleted` | `{ response, id, message, meta }` | Dispatched upon successful server deletion. `response` is the (possibly null) parsed body; `message` is `null` on a 204. Echoes `detail.meta` untouched, `null` if not provided. |
153
+ | `ln-api-connector:bulk-deleted`| `{ response, ids, message, meta }` | Dispatched upon successful server bulk deletion. Same `message` pass-through as the others. Echoes `detail.meta` untouched, `null` if not provided. |
154
+ | `ln-api-connector:error` | `{ action, error, status, ..., meta }` | Dispatched on failures. Includes `conflictData` if status is 409. `status` is always present (`0` for network failure). Echoes `detail.meta` from the triggering request untouched, `null` if not provided. |
155
+ | `ln-api-connector:config-changed` | `{ baseUrl, path, headers }` | Dispatched when configuration attributes are mutated. |
156
+
157
+ ---
158
+
159
+ ## Mutation Response Envelope
160
+
161
+ The three mutation response handlers (`create`/`update`/`delete`, and
162
+ `bulk-delete` for symmetry) presence-check the parsed JSON body for an
163
+ optional `{ message, content }` envelope, mirroring the unwrap `ln-ajax`
164
+ already does for HTML fragment responses (`ln-ajax.js:160` `if (data.content)`):
165
+
166
+ ```json
167
+ {
168
+ "message": { "type": "success", "title": "Saved", "body": "Document created" },
169
+ "content": { "id": 42, "title": "New Document", "status": "Draft" }
170
+ }
171
+ ```
172
+
173
+ - **`content`** — the record payload. `ln-api-connector:created`/`:updated`
174
+ unwrap `body.content` (presence-checked with `!== undefined`) into
175
+ `detail.record`; if the response has no `content` key, the bare body is
176
+ used as-is (back-compat with a plain-record response).
177
+ - **`content` duality** — this is the *same* envelope shape `ln-ajax` consumes,
178
+ just with a different content type: `ln-ajax` treats `content` as an
179
+ `{ targetId: html }` map for DOM fragment swaps, while `ln-api-connector`
180
+ treats it as JSON record data. One envelope, two consumers, two
181
+ interpretations of the payload.
182
+ - **`message` PRESENCE = toast opt-in.** If `message` is absent (or the whole
183
+ body has no envelope), `detail.message` is `null` and nothing renders. There
184
+ is no hardcoded toast text at the connector layer — the backend authors
185
+ the entire user-facing string.
186
+ - **The connector passes `message` opaquely** — it does not read `message.type`/
187
+ `title`/`body` itself. That happens one level up, in whichever consumer
188
+ turns it into an `ln-toast:enqueue` (see `js/ln-data-coordinator/README.md`
189
+ → Toasts).
190
+ - **204 caveat.** `_resolve` returns `null` for a 204 response (no body to
191
+ parse), so a DELETE answering `204 No Content` has no envelope and no
192
+ `message` — no toast fires. A backend that wants a delete confirmation
193
+ toast must answer `200 OK` with the envelope (`content` can be `null`).
194
+
195
+ Nothing else in the connector is touched by this: the JS API methods
196
+ (`create`/`update`/`delete`/`bulkDelete`), `_resolve`, config, and the error
197
+ path are unchanged — the unwrap happens only in the DOM-event handlers, at
198
+ the point the parsed body arrives.
199
+
200
+ ---
201
+
202
+ ## 3-Tier Integration Architecture (Coordinator Example)
203
+
204
+ Here is how a parent Coordinator links a Cache Store and the API Connector using
205
+ standard DOM events — parallel fan-out (local store write + remote connector
206
+ call happen from the same intake handler), no `_pending`/confirm/revert, and
207
+ success toasts riding the envelope's `message`:
208
+
209
+ ```javascript
210
+ (function () {
211
+ const parent = document.querySelector('[data-ln-data-coordinator="documents"]');
212
+ const storeEl = parent.querySelector('[data-ln-data-store]');
213
+ const connectorEl = parent.querySelector('[data-ln-api-connector]');
214
+
215
+ if (!storeEl || !connectorEl) return;
216
+
217
+ // 1. Storage needs remote sync -> forward request to Connector
218
+ storeEl.addEventListener('ln-data-store:request-remote-sync', function (e) {
219
+ connectorEl.dispatchEvent(new CustomEvent('ln-api-connector:request-sync', {
220
+ detail: { since: e.detail.since }
221
+ }));
222
+ });
223
+
224
+ // 2. Connector finishes sync -> feed delta back to Storage
225
+ connectorEl.addEventListener('ln-api-connector:fetched', function (e) {
226
+ const payload = e.detail.data;
227
+ storeEl.lnDataStore.applySync(payload.data, payload.deleted || [], payload.synced_at);
228
+ });
229
+
230
+ // 3. Intake fans out in parallel: local optimistic write AND remote call,
231
+ // from the same handler — not chained off each other.
232
+ const tempId = '_temp_' + crypto.randomUUID();
233
+ storeEl.dispatchEvent(new CustomEvent('ln-data-store:request-create', { detail: { tempId, data: { title: 'New Document' } } }));
234
+ connectorEl.dispatchEvent(new CustomEvent('ln-api-connector:request-create', {
235
+ detail: { data: { title: 'New Document' }, tempId: tempId }
236
+ }));
237
+
238
+ // 4. Connector confirms creation -> ordinary id-swap update (no confirmMutation)
239
+ connectorEl.addEventListener('ln-api-connector:created', function (e) {
240
+ storeEl.dispatchEvent(new CustomEvent('ln-data-store:request-update', {
241
+ detail: { id: e.detail.tempId, data: e.detail.record }
242
+ }));
243
+ // Success toast rides the envelope's message — no hardcoded string.
244
+ if (e.detail.message) {
245
+ window.dispatchEvent(new CustomEvent('ln-toast:enqueue', {
246
+ detail: { type: e.detail.message.type || 'success', title: e.detail.message.title || '', message: e.detail.message.body || '' }
247
+ }));
248
+ }
249
+ });
250
+
251
+ // 5. Errors are classified deterministic vs transient — never a blind revert.
252
+ // status 0/5xx -> transient (retry via queue, or leave local on single attempt);
253
+ // status 401/419 -> auth (pause queue); status 409/4xx -> deterministic (never retry).
254
+ connectorEl.addEventListener('ln-api-connector:error', function (e) {
255
+ if (e.detail.action === 'create' && e.detail.status >= 400 && e.detail.status < 500) {
256
+ storeEl.dispatchEvent(new CustomEvent('ln-data-store:request-delete', { detail: { id: e.detail.tempId } }));
257
+ }
258
+ // ... see ln-data-coordinator README's "Error reconciliation policy" for the full table
259
+ });
260
+ })();
261
+ ```
@@ -1 +1 @@
1
- (function(){"use strict";function c(i,o,d){i.dispatchEvent(new CustomEvent(o,{bubbles:!0,detail:d||{}}))}function L(i,o){if(!document.body){document.addEventListener("DOMContentLoaded",function(){L(i,o)}),console.warn("["+o+'] Script loaded before <body> — add "defer" to your <script> tag');return}i()}function g(i,o,d,u){if(i.nodeType!==1)return;const m=o.indexOf("[")!==-1||o.indexOf(".")!==-1||o.indexOf("#")!==-1?o:"["+o+"]",e=Array.from(i.querySelectorAll(m));i.matches&&i.matches(m)&&e.push(i);for(const n of e)n[d]||(n[d]=new u(n))}function x(i,o,d,u,f={}){const m=f.extraAttributes||[],e=f.onAttributeChange||null,n=f.onInit||null;function t(r){const a=r||document.body;g(a,i,o,d),n&&n(a)}return L(function(){const r=new MutationObserver(function(s){for(let b=0;b<s.length;b++){const l=s[b];if(l.type==="childList"){for(let p=0;p<l.addedNodes.length;p++){const h=l.addedNodes[p];h.nodeType===1&&(g(h,i,o,d),n&&n(h))}for(let p=0;p<l.removedNodes.length;p++){const h=l.removedNodes[p];if(h.nodeType===1){const C=i.indexOf("[")!==-1||i.indexOf(".")!==-1||i.indexOf("#")!==-1?i:"["+i+"]",E=Array.from(h.querySelectorAll(C));h.matches&&h.matches(C)&&E.push(h);for(let v=0;v<E.length;v++){const O=E[v];if(!document.contains(O)){const T=O[o];T&&typeof T.destroy=="function"&&T.destroy()}}}}}else l.type==="attributes"&&(e&&l.target[o]?e(l.target,l.attributeName):(g(l.target,i,o,d),n&&n(l.target)))}});let a=[];if(i.indexOf("[")!==-1){const s=/\[([\w-]+)/g;let b;for(;(b=s.exec(i))!==null;)a.push(b[1])}else a.push(i);r.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:a.concat(m)})},u),window[o]=t,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){t(document.body)}):t(document.body),t}function y(...i){return i.filter(o=>o!=null&&o!=="").map((o,d)=>d===0?o.replace(/\/+$/,""):o.replace(/^\/+/,"").replace(/\/+$/,"")).filter(Boolean).join("/")}function w(i,o){return Object.assign({"Content-Type":"application/json",Accept:"application/json"},i,null)}function A(i,o="ln-core"){try{return i?JSON.parse(i):{}}catch(d){return console.error(`[${o}] Invalid headers JSON:`,d),{}}}const _={};function D(i,o){_[i]=o}function k(i){return _[i]||{ingress:o=>o,egress:o=>o}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=D,window.lnCore.getDataMapper=k),(function(){const i="data-ln-api-connector",o="lnApiConnector",d="lnConnector";if(window[o]!==void 0)return;function u(e){return this.dom=e,e[o]=this,e[d]=this,this.refreshConfig(),this._handlers=null,f(this),this}u.prototype.refreshConfig=function(){const e=this.dom;this.baseUrl=e.getAttribute("data-ln-api-base-url")||"",this.path=e.getAttribute("data-ln-api-path")||"",this.credentials="same-origin";const n=e.getAttribute("data-ln-api-headers")||"";this.headers=A(n,"ln-api-connector"),(n.toLowerCase().includes("authorization")||n.toLowerCase().includes("bearer")||n.toLowerCase().includes("basic"))&&console.warn("[ln-api-connector] Security Warning: Sensitive authorization credentials detected in data-ln-api-headers attribute. Storing secrets in HTML DOM attributes is highly discouraged and vulnerable to XSS credential extraction. Please use HttpOnly session cookies or a Backend Proxy Gateway instead."),c(e,"ln-api-connector:config-changed",{baseUrl:this.baseUrl,path:this.path,headers:this.headers})},u.prototype.fetchDelta=function(e){const n=this;let t=y(n.baseUrl,n.path);return e!=null&&e!==""&&(t+=(t.indexOf("?")!==-1?"&":"?")+"since="+encodeURIComponent(e)),window.fetch(t,{method:"GET",headers:w(n.headers),credentials:n.credentials}).then(r=>{if(!r.ok)throw new Error("HTTP "+r.status+": "+r.statusText);return r.json()})},u.prototype.create=function(e){const n=this;return window.fetch(y(n.baseUrl,n.path),{method:"POST",headers:w(n.headers),credentials:n.credentials,body:JSON.stringify(e)}).then(t=>{if(!t.ok)throw new Error("HTTP "+t.status+": "+t.statusText);return t.json()})},u.prototype.update=function(e,n){const t=this;return window.fetch(y(t.baseUrl,t.path,e),{method:"PUT",headers:w(t.headers),credentials:t.credentials,body:JSON.stringify(n)}).then(r=>{if(r.ok)return r.json();if(r.status===409)return r.json().then(a=>{const s=new Error("Conflict");throw s.status=409,s.data=a,s});throw new Error("HTTP "+r.status+": "+r.statusText)})},u.prototype.delete=function(e){const n=this;return window.fetch(y(n.baseUrl,n.path,e),{method:"DELETE",headers:w(n.headers),credentials:n.credentials}).then(t=>{if(!t.ok)throw new Error("HTTP "+t.status+": "+t.statusText);return t.json()})},u.prototype.bulkDelete=function(e){const n=this;return window.fetch(y(n.baseUrl,n.path)+"/bulk-delete",{method:"DELETE",headers:w(n.headers),credentials:n.credentials,body:JSON.stringify({ids:e})}).then(t=>{if(!t.ok)throw new Error("HTTP "+t.status+": "+t.statusText);return t.json()})};function f(e){e._handlers={sync:function(t){const r=t.detail||{};e.fetchDelta(r.since).then(function(a){c(e.dom,"ln-api-connector:fetched",{data:a,since:r.since})}).catch(function(a){c(e.dom,"ln-api-connector:error",{action:"sync",error:a.message,status:a.status||0,since:r.since})})},create:function(t){const r=t.detail||{};e.create(r.data).then(function(a){c(e.dom,"ln-api-connector:created",{record:a,tempId:r.tempId})}).catch(function(a){c(e.dom,"ln-api-connector:error",{action:"create",error:a.message,status:a.status||0,tempId:r.tempId})})},update:function(t){const r=t.detail||{},a=Object.assign({},r.data);r.expected_version!==void 0&&(a.expected_version=r.expected_version),e.update(r.id,a).then(function(s){c(e.dom,"ln-api-connector:updated",{record:s,id:r.id})}).catch(function(s){c(e.dom,"ln-api-connector:error",{action:"update",error:s.message,status:s.status||0,id:r.id,conflictData:s.status===409?s.data:null})})},delete:function(t){const r=t.detail||{};e.delete(r.id).then(function(a){c(e.dom,"ln-api-connector:deleted",{response:a,id:r.id})}).catch(function(a){c(e.dom,"ln-api-connector:error",{action:"delete",error:a.message,status:a.status||0,id:r.id})})},bulkDelete:function(t){const r=t.detail||{};e.bulkDelete(r.ids).then(function(a){c(e.dom,"ln-api-connector:bulk-deleted",{response:a,ids:r.ids})}).catch(function(a){c(e.dom,"ln-api-connector:error",{action:"bulk-delete",error:a.message,status:a.status||0,ids:r.ids})})}},["ln-api-connector","ln-rest-connector"].forEach(function(t){e.dom.addEventListener(t+":request-sync",e._handlers.sync),e.dom.addEventListener(t+":request-fetch",e._handlers.sync),e.dom.addEventListener(t+":request-create",e._handlers.create),e.dom.addEventListener(t+":request-update",e._handlers.update),e.dom.addEventListener(t+":request-delete",e._handlers.delete),e.dom.addEventListener(t+":request-bulk-delete",e._handlers.bulkDelete)})}u.prototype.destroy=function(){if(!this.dom[o])return;const e=this;e._handlers&&(["ln-api-connector","ln-rest-connector"].forEach(function(t){e.dom.removeEventListener(t+":request-sync",e._handlers.sync),e.dom.removeEventListener(t+":request-fetch",e._handlers.sync),e.dom.removeEventListener(t+":request-create",e._handlers.create),e.dom.removeEventListener(t+":request-update",e._handlers.update),e.dom.removeEventListener(t+":request-delete",e._handlers.delete),e.dom.removeEventListener(t+":request-bulk-delete",e._handlers.bulkDelete)}),e._handlers=null),c(this.dom,"ln-api-connector:destroyed",{target:this.dom}),delete this.dom[o],delete this.dom[d]};function m(e){const n=e[o];n&&n.refreshConfig()}x(i,o,u,"ln-api-connector",{extraAttributes:["data-ln-api-base-url","data-ln-api-path","data-ln-api-headers"],onAttributeChange:m})})()})();
1
+ (function(){"use strict";if(typeof window<"u"){const t=console.warn;console.warn=function(...i){typeof i[0]=="string"&&(i[0].startsWith("[ln-")||i[0].startsWith("[lnCore"))&&!(document.documentElement.hasAttribute("data-ln-debug")||document.body&&document.body.hasAttribute("data-ln-debug"))||t.apply(console,i)}}const b={};function j(t,i){b[t]||(b[t]=document.querySelector('[data-ln-template="'+t+'"]'));const s=b[t];return s?s.content.cloneNode(!0):(console.warn("["+(i||"ln-core")+'] Template "'+t+'" not found'),null)}function p(t,i,s){t.dispatchEvent(new CustomEvent(i,{bubbles:!0,detail:s||{}}))}function L(t,i){if(!t||!i)return t;const s=t.querySelectorAll("[data-ln-field]");for(let l=0;l<s.length;l++){const e=s[l],a=e.getAttribute("data-ln-field");i[a]!=null&&(e.textContent=i[a])}const f=t.querySelectorAll("[data-ln-attr]");for(let l=0;l<f.length;l++){const e=f[l],a=e.getAttribute("data-ln-attr").split(",");for(let r=0;r<a.length;r++){const n=a[r].trim().split(":");if(n.length!==2)continue;const o=n[0].trim(),c=n[1].trim();i[c]!=null&&e.setAttribute(o,i[c])}}const d=t.querySelectorAll("[data-ln-show]");for(let l=0;l<d.length;l++){const e=d[l],a=e.getAttribute("data-ln-show");a in i&&e.classList.toggle("hidden",!i[a])}const u=t.querySelectorAll("[data-ln-class]");for(let l=0;l<u.length;l++){const e=u[l],a=e.getAttribute("data-ln-class").split(",");for(let r=0;r<a.length;r++){const n=a[r].trim().split(":");if(n.length!==2)continue;const o=n[0].trim(),c=n[1].trim();c in i&&e.classList.toggle(o,!!i[c])}}return t}function U(t,i){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:i??null,bubbles:!0}));const s=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let f=0;f<s.length;f++)s[f].dispatchEvent(new CustomEvent("ln-fill",{detail:i??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)L(t.target,t.detail);else{const i=t.target.querySelectorAll("[data-ln-field]");for(let s=0;s<i.length;s++)i[s].textContent=""}})));function E(t,i){if(!t||!i)return t;const s=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;s.nextNode();){const u=s.currentNode;u.textContent.indexOf("{{")!==-1&&(u.textContent=u.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(l,e){return i[e]!==void 0?i[e]:""}))}const f=function(u,l){return i[l]!==void 0?i[l]:""},d=Array.from(t.querySelectorAll("*"));t.nodeType===1&&d.push(t);for(let u=0;u<d.length;u++){const l=d[u],e=l.attributes;for(let a=0;a<e.length;a++){const r=e[a];r.value.indexOf("{{")!==-1&&l.setAttribute(r.name,r.value.replace(/\{\{\s*(\w+)\s*\}\}/g,f))}}return t}function N(t,i,s,f,d,u){const l={};for(let a=0;a<t.children.length;a++){const r=t.children[a],n=r.getAttribute("data-ln-key");n&&(l[n]=r)}const e=document.createDocumentFragment();for(let a=0;a<i.length;a++){const r=i[a],n=String(f(r));let o=l[n];if(o)d(o,r,a);else{const c=j(s,u);if(!c||(E(c,r),o=c.firstElementChild,!o))continue;o.setAttribute("data-ln-key",n),d(o,r,a)}e.appendChild(o)}t.textContent="",t.appendChild(e)}function S(t,i){if(!document.body){document.addEventListener("DOMContentLoaded",function(){S(t,i)}),console.warn("["+i+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function y(t,i,s,f){if(t.nodeType!==1)return;const u=i.indexOf("[")!==-1||i.indexOf(".")!==-1||i.indexOf("#")!==-1?i:"["+i+"]",l=Array.from(t.querySelectorAll(u));t.matches&&t.matches(u)&&l.push(t);for(const e of l)e[s]||(e[s]=new f(e))}function H(t,i,s,f,d={}){const u=d.extraAttributes||[],l=d.onAttributeChange||null,e=d.onInit||null;function a(r){const n=r||document.body;y(n,t,i,s),e&&e(n)}return S(function(){const r=new MutationObserver(function(o){for(let c=0;c<o.length;c++){const h=o[c];if(h.type==="childList"){for(let m=0;m<h.addedNodes.length;m++){const g=h.addedNodes[m];g.nodeType===1&&(y(g,t,i,s),e&&e(g))}for(let m=0;m<h.removedNodes.length;m++){const g=h.removedNodes[m];if(g.nodeType===1){const D=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",_=Array.from(g.querySelectorAll(D));g.matches&&g.matches(D)&&_.push(g);for(let v=0;v<_.length;v++){const T=_[v];if(!document.contains(T)){const A=T[i];A&&typeof A.destroy=="function"&&A.destroy()}}}}}else h.type==="attributes"&&(l&&h.target[i]?l(h.target,h.attributeName):(y(h.target,t,i,s),e&&e(h.target)))}});let n=[];if(t.indexOf("[")!==-1){const o=/\[([\w-]+)/g;let c;for(;(c=o.exec(t))!==null;)n.push(c[1])}else n.push(t);r.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:n.concat(u)})},f),window[i]=a,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){a(document.body)}):a(document.body),a}function w(...t){return t.filter(i=>i!=null&&i!=="").map((i,s)=>s===0?i.replace(/\/+$/,""):i.replace(/^\/+/,"").replace(/\/+$/,"")).filter(Boolean).join("/")}function I(t,i){return Object.assign({"Content-Type":"application/json",Accept:"application/json"},t,null)}function M(t,i="ln-core"){try{return t?JSON.parse(t):{}}catch(s){return console.error(`[${i}] Invalid headers JSON:`,s),{}}}const O={};function K(t,i){O[t]=i}function F(t){return O[t]||{ingress:i=>i,egress:i=>i}}const q={};function k(t,i){if(!t||typeof i!="object")return;const s=t.toLowerCase().split("-")[0];q[s]=i}function W(t){if(!t)return null;const i=t.toLowerCase().split("-")[0];return q[i]||null}k("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=K,window.lnCore.getDataMapper=F,window.lnCore.registerLocaleFallback=k,window.lnCore.getLocaleFallback=W,window.lnCore.fillTemplate=E,window.lnCore.fill=L,window.lnCore.lnFill=U,window.lnCore.renderList=N);function x(t){return(t||"").replace(/^#/,"")}function C(t){const i=t===void 0?location.hash:t,s={},f=x(i);if(!f)return s;const d=f.split("&");for(let u=0;u<d.length;u++){const l=d[u];if(!l)continue;const e=l.indexOf(":"),a=e>-1?l.slice(0,e):l,r=e>-1?l.slice(e+1):"";if(a)try{s[a]=decodeURIComponent(r)}catch{s[a]=r}}return s}function B(t){if(!t)return null;const i=C();return t in i?i[t]:null}function G(t,i){if(!t)return;const s=C();i==null?delete s[t]:s[t]=String(i);const d=Object.keys(s).map(function(u){const l=s[u];return l===""?u:u+":"+encodeURIComponent(l)}).join("&");x(location.hash)!==d&&(location.hash=d)}function R(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=C,window.lnCore.hashGet=B,window.lnCore.hashSet=G,window.lnCore.hashLinkClick=R),(function(){const t="data-ln-api-connector",i="lnApiConnector",s="lnConnector";if(window[i]!==void 0)return;function f(e){return e.ok?e.status===204?null:e.json():e.json().catch(()=>null).then(a=>{const r=new Error("HTTP "+e.status+": "+e.statusText);throw r.status=e.status,r.data=a,r})}function d(e){return this.dom=e,e[i]=this,e[s]=this,this.refreshConfig(),this._handlers=null,u(this),this}d.prototype.refreshConfig=function(){const e=this.dom;this.baseUrl=e.getAttribute("data-ln-api-base-url")||"",this.path=e.getAttribute("data-ln-api-path")||"",this.credentials="same-origin",this.paramKeys={offset:e.getAttribute("data-ln-api-param-offset")||"offset",limit:e.getAttribute("data-ln-api-param-limit")||"limit",search:e.getAttribute("data-ln-api-param-search")||"search",sortField:e.getAttribute("data-ln-api-param-sort-field")||"sort_field",sortDir:e.getAttribute("data-ln-api-param-sort-dir")||"sort_dir"};const a=e.getAttribute("data-ln-api-headers")||"";this.headers=M(a,"ln-api-connector"),(a.toLowerCase().includes("authorization")||a.toLowerCase().includes("bearer")||a.toLowerCase().includes("basic"))&&console.warn("[ln-api-connector] Security Warning: Sensitive authorization credentials detected in data-ln-api-headers attribute. Storing secrets in HTML DOM attributes is highly discouraged and vulnerable to XSS credential extraction. Please use HttpOnly session cookies or a Backend Proxy Gateway instead."),p(e,"ln-api-connector:config-changed",{baseUrl:this.baseUrl,path:this.path,headers:this.headers,paramKeys:this.paramKeys})},d.prototype._reqHeaders=function(e){const a=Object.assign({},I(this.headers),{"X-LN-Response":"data"});return e&&(a["Idempotency-Key"]=e),a},d.prototype.fetchDelta=function(e){const a=this;let r=w(a.baseUrl,a.path);return e!=null&&e!==""&&(r+=(r.indexOf("?")!==-1?"&":"?")+"since="+encodeURIComponent(e)),window.fetch(r,{method:"GET",headers:a._reqHeaders(),credentials:a.credentials}).then(f)},d.prototype.query=function(e){const a=this;e=e||{};let r=w(a.baseUrl,a.path);const n=a.paramKeys||{offset:"offset",limit:"limit",search:"search",sortField:"sort_field",sortDir:"sort_dir"},o=new URLSearchParams;e.search&&o.append(n.search,e.search),e.offset!=null&&o.append(n.offset,e.offset),e.limit!=null&&o.append(n.limit,e.limit),e.sort&&e.sort.field&&e.sort.direction&&(o.append(n.sortField,e.sort.field),o.append(n.sortDir,e.sort.direction)),e.filters&&typeof e.filters=="object"&&Object.keys(e.filters).forEach(h=>{const m=e.filters[h];Array.isArray(m)&&m.length>0&&o.append(h,m.join(","))});const c=o.toString();return c&&(r+=(r.indexOf("?")!==-1?"&":"?")+c),window.fetch(r,{method:"GET",headers:a._reqHeaders(),credentials:a.credentials}).then(f)},d.prototype.create=function(e,a,r){const n=this;return window.fetch(w(n.baseUrl,a||n.path),{method:"POST",headers:n._reqHeaders(r),credentials:n.credentials,body:JSON.stringify(e)}).then(f)},d.prototype.update=function(e,a,r,n,o){const c=this;r!=null&&(a=Object.assign({},a,{expected_version:r}));const h=n?w(c.baseUrl,n):w(c.baseUrl,c.path,e);return window.fetch(h,{method:"PUT",headers:c._reqHeaders(o),credentials:c.credentials,body:JSON.stringify(a)}).then(f)},d.prototype.delete=function(e,a,r){const n=this;return window.fetch(w(n.baseUrl,a||n.path,e),{method:"DELETE",headers:n._reqHeaders(r),credentials:n.credentials}).then(f)},d.prototype.bulkDelete=function(e,a,r){const n=this;return window.fetch(w(n.baseUrl,a||n.path)+"/bulk-delete",{method:"DELETE",headers:n._reqHeaders(r),credentials:n.credentials,body:JSON.stringify({ids:e})}).then(f)};function u(e){e._handlers={sync:function(r){const n=r.detail||{};e.fetchDelta(n.since).then(function(o){p(e.dom,"ln-api-connector:fetched",{data:o,since:n.since,meta:n.meta||null})}).catch(function(o){p(e.dom,"ln-api-connector:error",{action:"sync",error:o.message,status:o.status||0,data:o.data||null,since:n.since,meta:n.meta||null})})},query:function(r){const n=r.detail||{},o=n.query||n;e.query(o).then(function(c){const h=c||{};p(e.dom,"ln-api-connector:fetched",{data:h.data||(Array.isArray(h)?h:[]),total:h.total,filtered:h.filtered,offset:o.offset,queryGen:o.queryGen,meta:n.meta||null})}).catch(function(c){p(e.dom,"ln-api-connector:error",{action:"query",error:c.message,status:c.status||0,data:c.data||null,meta:n.meta||null})})},create:function(r){const n=r.detail||{};e.create(n.data,n.url,n.idempotencyKey).then(function(o){const c=o&&o.content!==void 0?o.content:o,h=o&&o.message?o.message:null;p(e.dom,"ln-api-connector:created",{record:c,tempId:n.tempId,message:h,meta:n.meta||null})}).catch(function(o){p(e.dom,"ln-api-connector:error",{action:"create",error:o.message,status:o.status||0,data:o.data||null,tempId:n.tempId,meta:n.meta||null})})},update:function(r){const n=r.detail||{};e.update(n.id,n.data,n.expected_version,n.url,n.idempotencyKey).then(function(o){const c=o&&o.content!==void 0?o.content:o,h=o&&o.message?o.message:null;p(e.dom,"ln-api-connector:updated",{record:c,id:n.id,message:h,meta:n.meta||null})}).catch(function(o){p(e.dom,"ln-api-connector:error",{action:"update",error:o.message,status:o.status||0,data:o.data||null,id:n.id,conflictData:o.status===409?o.data:null,meta:n.meta||null})})},delete:function(r){const n=r.detail||{};e.delete(n.id,n.url,n.idempotencyKey).then(function(o){const c=o&&o.message?o.message:null;p(e.dom,"ln-api-connector:deleted",{response:o,id:n.id,message:c,meta:n.meta||null})}).catch(function(o){p(e.dom,"ln-api-connector:error",{action:"delete",error:o.message,status:o.status||0,data:o.data||null,id:n.id,meta:n.meta||null})})},bulkDelete:function(r){const n=r.detail||{};e.bulkDelete(n.ids,n.url,n.idempotencyKey).then(function(o){const c=o&&o.message?o.message:null;p(e.dom,"ln-api-connector:bulk-deleted",{response:o,ids:n.ids,message:c,meta:n.meta||null})}).catch(function(o){p(e.dom,"ln-api-connector:error",{action:"bulk-delete",error:o.message,status:o.status||0,data:o.data||null,ids:n.ids,meta:n.meta||null})})}},["ln-api-connector","ln-rest-connector"].forEach(function(r){e.dom.addEventListener(r+":request-sync",e._handlers.sync),e.dom.addEventListener(r+":request-query",e._handlers.query),e.dom.addEventListener(r+":request-fetch",e._handlers.query),e.dom.addEventListener(r+":request-create",e._handlers.create),e.dom.addEventListener(r+":request-update",e._handlers.update),e.dom.addEventListener(r+":request-delete",e._handlers.delete),e.dom.addEventListener(r+":request-bulk-delete",e._handlers.bulkDelete)})}d.prototype.destroy=function(){if(!this.dom[i])return;const e=this;e._handlers&&(["ln-api-connector","ln-rest-connector"].forEach(function(r){e.dom.removeEventListener(r+":request-sync",e._handlers.sync),e.dom.removeEventListener(r+":request-query",e._handlers.query),e.dom.removeEventListener(r+":request-fetch",e._handlers.query),e.dom.removeEventListener(r+":request-create",e._handlers.create),e.dom.removeEventListener(r+":request-update",e._handlers.update),e.dom.removeEventListener(r+":request-delete",e._handlers.delete),e.dom.removeEventListener(r+":request-bulk-delete",e._handlers.bulkDelete)}),e._handlers=null),p(this.dom,"ln-api-connector:destroyed",{target:this.dom}),delete this.dom[i],delete this.dom[s]};function l(e){const a=e[i];a&&a.refreshConfig()}H(t,i,d,"ln-api-connector",{extraAttributes:["data-ln-api-base-url","data-ln-api-path","data-ln-api-headers","data-ln-api-param-offset","data-ln-api-param-limit","data-ln-api-param-search","data-ln-api-param-sort-field","data-ln-api-param-sort-dir"],onAttributeChange:l})})()})();