@livenetworks/ashlar 1.4.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/README.md +189 -177
  2. package/js/COMPONENTS.md +55 -49
  3. package/js/index.js +22 -5
  4. package/js/ln-accordion/README.md +186 -137
  5. package/js/ln-accordion/ln-accordion.js +1 -1
  6. package/js/ln-ajax/README.md +73 -4
  7. package/js/ln-ajax/ln-ajax.js +1 -1
  8. package/js/ln-ajax/src/ln-ajax.js +13 -13
  9. package/js/ln-api-connector/README.md +261 -150
  10. package/js/ln-api-connector/ln-api-connector.js +1 -1
  11. package/js/ln-api-connector/src/ln-api-connector.js +368 -265
  12. package/js/ln-api-queue/README.md +219 -0
  13. package/js/ln-api-queue/ln-api-queue.js +1 -0
  14. package/js/ln-api-queue/src/ln-api-queue.js +320 -0
  15. package/js/ln-api-queue/src/queue-storage.js +455 -0
  16. package/js/ln-autoresize/README.md +47 -0
  17. package/js/ln-autoresize/ln-autoresize.js +1 -1
  18. package/js/ln-autosave/README.md +33 -1
  19. package/js/ln-autosave/ln-autosave.js +1 -1
  20. package/js/ln-autosave/src/ln-autosave.js +4 -4
  21. package/js/ln-circular-progress/README.md +27 -13
  22. package/js/ln-circular-progress/ln-circular-progress.js +1 -1
  23. package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
  24. package/js/ln-confirm/README.md +162 -86
  25. package/js/ln-confirm/_ln-confirm.scss +11 -0
  26. package/js/ln-confirm/ln-confirm.js +1 -1
  27. package/js/ln-confirm/src/ln-confirm.js +77 -41
  28. package/js/ln-core/README.md +896 -0
  29. package/js/ln-core/hash.js +92 -0
  30. package/js/ln-core/helpers.js +337 -8
  31. package/js/ln-core/index.js +4 -2
  32. package/js/ln-core/persist.js +29 -3
  33. package/js/ln-core/positioning.js +1 -35
  34. package/js/ln-core/window-cache.js +213 -0
  35. package/js/ln-couchdb-connector/README.md +177 -156
  36. package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
  37. package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
  38. package/js/ln-data-coordinator/README.md +536 -165
  39. package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
  40. package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
  41. package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
  42. package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
  43. package/js/ln-data-store/README.md +241 -94
  44. package/js/ln-data-store/ln-data-store.js +1 -1
  45. package/js/ln-data-store/src/ln-data-store.js +694 -699
  46. package/js/ln-date/README.md +39 -1
  47. package/js/ln-date/ln-date-dev.scss +14 -0
  48. package/js/ln-date/ln-date.js +1 -1
  49. package/js/ln-date/src/ln-date.js +236 -178
  50. package/js/ln-debug/README.md +58 -0
  51. package/js/ln-debug/ln-debug.js +1 -0
  52. package/js/ln-debug/src/ln-debug.js +19 -0
  53. package/js/ln-dropdown/README.md +31 -8
  54. package/js/ln-dropdown/ln-dropdown.js +1 -1
  55. package/js/ln-dropdown/src/ln-dropdown.js +30 -13
  56. package/js/ln-editor/README.md +274 -0
  57. package/js/ln-editor/ln-editor.js +1 -0
  58. package/js/ln-editor/ln-editor.scss +43 -0
  59. package/js/ln-editor/src/ln-editor.js +619 -0
  60. package/js/ln-external-links/README.md +44 -5
  61. package/js/ln-external-links/ln-external-links.js +1 -1
  62. package/js/ln-fill/README.md +199 -0
  63. package/js/ln-fill/ln-fill.js +1 -0
  64. package/js/ln-fill/src/ln-fill.js +87 -0
  65. package/js/ln-filter/README.md +189 -99
  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 -55
  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 -0
  173. package/scss/components/_form.scss +7 -0
  174. package/scss/components/_ln-table.scss +14 -5
  175. package/scss/components/_sort.scss +10 -0
  176. package/scss/components/_table.scss +68 -1
  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 +48 -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 +47 -0
  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 +4 -1
  198. package/scss/config/mixins/_ln-table.scss +51 -93
  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 +232 -38
  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 +39 -0
  209. package/scss/ln-ashlar.scss +8 -2
  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 -1103
  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 -168
  218. package/scss/components/_data-table.scss +0 -83
  219. package/scss/config/mixins/_data-table.scss +0 -346
@@ -1,154 +1,185 @@
1
- # ln-modal
2
-
3
- > Focus-gated viewport-blocking dialog overlays, managed reactively via the DOM.
4
-
5
- ---
6
-
7
- ## 1. Philosophy & The Modal Mindset
8
-
9
- In `ln-ashlar`, the core design principle is **orthogonality**. Rather than creating a heavy component that bundles form handlers, visual layouts, focus traps, backdrop styling, and dimensions, `ln-modal` splits them cleanly:
10
-
11
- 1. **State & Accessibility (JavaScript)**: The `ln-modal` script (145 lines) only manages binary `open` / `close` state, suppresses parent `<body>` scrolling, intercepts tab navigation to trap focus inside the modal, closes topmost modals on ESC key, and restores focus back to the trigger on close.
12
- 2. **The Content Root (HTML)**: The modal content root is ALWAYS a `<form>`. The form IS the panel — there are no redundant BEM wrappers like `.ln-modal__content`. Cancel and submit buttons live directly inside the form.
13
- 3. **Visual Presentation & Sizing (CSS)**: Overlay backdrops, Sticky headers/footers, and scrollable body areas are styled using Vanilla CSS. Sizing variants (`modal-sm|md|lg|xl`) are applied via SCSS mixins on `> form`, keeping markup completely clean.
14
-
15
- ---
16
-
17
- ## 2. Minimal Blueprint
18
-
19
- Triggers and modals are paired by ID. The overlay has `class="ln-modal"` and `data-ln-modal`. The direct child must always be a `<form>`.
20
-
21
- ```html
22
- <!-- Trigger button -->
23
- <button data-ln-modal-for="user-modal">Add User</button>
24
-
25
- <!-- Modal overlay -->
26
- <div class="ln-modal" data-ln-modal id="user-modal">
27
- <form>
28
- <!-- Header -->
29
- <header>
30
- <h3>Add User</h3>
31
- <button type="button" data-ln-modal-close aria-label="Close">
32
- <svg class="ln-icon" aria-hidden="true"><use href="#ln-x"></use></svg>
33
- </button>
34
- </header>
35
-
36
- <!-- Scrollable content -->
37
- <main>
38
- <label>Name <input type="text" name="name" autofocus></label>
39
- </main>
40
-
41
- <!-- Sticky footer -->
42
- <footer>
43
- <button type="button" data-ln-modal-close>Cancel</button>
44
- <button type="submit">Save</button>
45
- </footer>
46
- </form>
47
- </div>
48
- ```
49
-
50
- ### Key Anatomy Rules
51
- - **The Overlay (`data-ln-modal`)**: Driven by the value `"open"` (open) and `"close"` or empty (closed).
52
- - **The Trigger (`data-ln-modal-for="id"`)**: Placed on buttons/links to toggle modal display.
53
- - **The Dismiss button (`data-ln-modal-close`)**: Placed on cancel or close buttons. Always needs `type="button"` inside a form.
54
- - **Focus Override (`autofocus`)**: Place on any form field to override the default behavior of focusing the first input on open.
55
-
56
- ---
57
-
58
- ## 3. Declarative API & State Contract
59
-
60
- There are no imperative JavaScript methods (like `open()` or `close()`) on the component instance. **The HTML attribute is the sole contract.**
61
-
62
- Triggers, backdrop dismissals, ESC handlers, and custom scripts all change state by writing the active attribute on the modal element:
63
-
64
- ```js
65
- const modal = document.getElementById('user-modal');
66
-
67
- // Open the modal
68
- modal.setAttribute('data-ln-modal', 'open');
69
-
70
- // Close the modal
71
- modal.setAttribute('data-ln-modal', 'close');
72
-
73
- // Read-only state query
74
- modal.lnModal.isOpen; // Returns true/false
75
- ```
76
-
77
- ### Attributes
78
- - `data-ln-modal`: Placed on the overlay element. Value `"open"` = open; `"close"` = closed.
79
- - `data-ln-modal-for="id"`: Placed on trigger elements referencing the modal ID.
80
- - `data-ln-modal-close`: Placed on any close trigger inside the modal.
81
-
82
- ---
83
-
84
- ## 4. Transition Events
85
-
86
- All events bubble. The dispatch target is the overlay element. Every event's `detail` carries `{ modalId, target }`.
87
-
88
- | Event | Cancelable | Dispatched When |
89
- |---|:---:|---|
90
- | **`ln-modal:before-open`** | **Yes** | Before opening. Calling `event.preventDefault()` cancels the transition and reverts the attribute. |
91
- | **`ln-modal:open`** | No | After modal is active, body scroll locked, and focus trapped. |
92
- | **`ln-modal:before-close`** | **Yes** | Before closing. Calling `event.preventDefault()` cancels the close and reverts the attribute. |
93
- | **`ln-modal:close`** | No | After modal is closed, scroll locks released, and focus restored. |
94
-
95
- ```js
96
- // Example: Block close transition if form is dirty (unsaved changes)
97
- const modal = document.getElementById('user-modal');
98
- modal.addEventListener('ln-modal:before-close', (e) => {
99
- if (formIsDirty()) {
100
- e.preventDefault(); // Reverts attribute back to "open"
101
- }
102
- });
103
- ```
104
-
105
- ---
106
-
107
- ## 5. Visual Sizing & SCSS Mixins
108
-
109
- Do not use visual layout utility classes in your markup. Apply structural sizing variants inside your SCSS to the modal form element:
110
-
111
- | Mixin | `max-width` | Ideal For |
112
- |---|---|---|
113
- | `@include modal-sm;` | `28rem` | Simple confirms, single-field inputs |
114
- | `@include modal-md;` | `32rem` | Standard 2-4 field forms (Default) |
115
- | `@include modal-lg;` | `42rem` | Wide multi-column forms, data lists |
116
- | `@include modal-xl;` | `48rem` | Detail previews, large forms |
117
-
118
- ```scss
119
- // Apply in project SCSS
120
- .ln-modal {
121
- @include modal-overlay;
122
-
123
- > form {
124
- @include modal-panel;
125
- @include modal-md; // default
126
- }
127
-
128
- &#user-modal > form {
129
- @include modal-lg; // Override specific modal width
130
- }
131
- }
132
- ```
133
-
134
- ---
135
-
136
- ## 6. Integration & Source Files
137
-
138
- - **Unified Bundle**: Loaded automatically with the main bundle:
139
- ```html
140
- <script src="dist/ln-ashlar.iife.js" defer></script>
141
- ```
142
- - **Standalone IIFE**: For lightweight pages, load the standalone, self-registering IIFE version:
143
- ```html
144
- <script src="js/ln-modal/ln-modal.js" defer></script>
145
- ```
146
- - **Active Source (ESM)**: Development source is located at [js/ln-modal/src/ln-modal.js](file:///c:/laragon/www/ln-ashlar/js/ln-modal/src/ln-modal.js).
147
-
148
- ---
149
-
150
- ## Related
151
- - **[`ln-confirm`](../ln-confirm/README.md)** — Two-click inline confirm actions (lightweight alternative to modals).
152
- - **[`ln-form`](../ln-form/README.md)** — Form serialization and success/error cascades.
153
- - **[`ln-validate`](../ln-validate/README.md)** — Declarative field-level constraints and visual errors.
154
- - **Architecture deep-dive** — [`docs/js/modal.md`](../../docs/js/modal.md).
1
+ # ln-modal
2
+
3
+ > Focus-gated viewport-blocking dialog overlays, managed reactively via the DOM.
4
+
5
+ ---
6
+
7
+ ## 1. Philosophy & The Modal Mindset
8
+
9
+ In `ln-ashlar`, the core design principle is **Three-Layer Architecture** and **Component Isolation**.
10
+
11
+ `ln-modal` is a **Layer 1 Simple Component**. It manages ONLY the overlay panel state and dialog accessibility. It is completely isolated and unaware of sibling components, form logic, or data-fetching pipelines.
12
+
13
+ 1. **State & Accessibility (JavaScript + native `<dialog>`)**: The `ln-modal` script manages the binary `open` / `close` attribute contract (`data-ln-modal="open|close"`) and suppresses `<body>` scrolling (toggling the `.ln-modal-open` class on `<body>`). Focus placement, native focus trapping, ESC dismissal, and focus restoration are delegated to the native `<dialog>` element via `showModal()`/`close()`.
14
+ 2. **The Content Root (HTML)**: The modal content root is structured in standard semantic HTML (such as `<dialog class="ln-modal" data-ln-modal>`).
15
+ 3. **Visual Presentation & Sizing (CSS)**: Overlay backdrops, sticky headers/footers, and scrollable body areas are styled using Vanilla CSS. Sizing variants (`modal-sm|md|lg|xl`) are applied via SCSS mixins on `> form`, keeping markup clean.
16
+ 4. **Layer Separation (Coordinators)**: `ln-modal` does NOT handle form submissions, auto-closing on AJAX success, validation error inspection, or DOM data population. Those responsibilities belong strictly to **Layer 2 Coordinators** (e.g. `ln-modal-coordinator` or `ln-data-coordinator`).
17
+
18
+ ---
19
+
20
+ ## 2. Minimal Blueprint
21
+
22
+ Triggers and modals are paired by ID. The overlay has `class="ln-modal"` and `data-ln-modal`.
23
+
24
+ ```html
25
+ <!-- Trigger button -->
26
+ <button data-ln-modal-for="user-modal">Add User</button>
27
+
28
+ <!-- Modal overlay -->
29
+ <dialog class="ln-modal" data-ln-modal id="user-modal">
30
+ <form>
31
+ <!-- Header -->
32
+ <header>
33
+ <h3>Add User</h3>
34
+ <button type="button" data-ln-modal-close aria-label="Close">
35
+ <svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-x"></use></svg>
36
+ </button>
37
+ </header>
38
+
39
+ <!-- Scrollable content -->
40
+ <main>
41
+ <label>Name <input type="text" name="name" autofocus></label>
42
+ </main>
43
+
44
+ <!-- Sticky footer -->
45
+ <footer>
46
+ <button type="button" data-ln-modal-close>Cancel</button>
47
+ <button type="submit">Save</button>
48
+ </footer>
49
+ </form>
50
+ </dialog>
51
+ ```
52
+
53
+ ### Key Anatomy Rules
54
+ - **The Overlay (`data-ln-modal`)**: Driven by the value `"open"` (open) and `"close"` (closed).
55
+ - **The Trigger (`data-ln-modal-for="id"`)**: Placed on buttons/links to request toggling modal display.
56
+ - **The Dismiss button (`data-ln-modal-close`)**: Placed on cancel or close buttons to request close.
57
+ - **Focus Override (`autofocus`)**: Place on any form field to override default focus placement on open.
58
+
59
+ ---
60
+
61
+ ## 3. Declarative API & State Contract
62
+
63
+ There are no imperative JavaScript mutation methods on the component instance. **The HTML attribute is the sole source of truth.**
64
+
65
+ Coordinators and external scripts request state changes by dispatching request events or writing the attribute on the modal element:
66
+
67
+ ```js
68
+ const modal = document.getElementById('user-modal');
69
+
70
+ // Request open via event
71
+ modal.dispatchEvent(new CustomEvent('ln-modal:request-open', { bubbles: true }));
72
+
73
+ // Or set attribute directly
74
+ modal.setAttribute('data-ln-modal', 'open');
75
+
76
+ // Request close via event
77
+ modal.dispatchEvent(new CustomEvent('ln-modal:request-close', { bubbles: true }));
78
+
79
+ // Read-only state query
80
+ const isOpen = modal.lnModal.isOpen; // Returns true/false
81
+ ```
82
+
83
+ ### Attributes
84
+ - `data-ln-modal`: Placed on the overlay element. Value `"open"` = open; `"close"` = closed.
85
+ - `data-ln-modal-for="id"`: Placed on trigger elements referencing the modal ID.
86
+ - `data-ln-modal-close`: Placed on any close trigger inside the modal.
87
+
88
+ ---
89
+
90
+ ## 4. Transition & Command Events
91
+
92
+ All events bubble (`bubbles: true`). The dispatch target is the overlay element. Every event's `detail` carries `{ modalId, target }`.
93
+
94
+ | Event | Direction | Cancelable | Dispatched / Handled When |
95
+ |---|:---:|:---:|---|
96
+ | **`ln-modal:request-open`** | Listens | No | Command event sent by coordinators/triggers to open the modal. |
97
+ | **`ln-modal:request-close`** | Listens | No | Command event sent by coordinators/triggers to close the modal. |
98
+ | **`ln-modal:before-open`** | Emits | **Yes** | Dispatched before opening. Calling `event.preventDefault()` cancels the transition. |
99
+ | **`ln-modal:open`** | Emits | No | Dispatched after modal is active, body scroll locked, and initial focus set. |
100
+ | **`ln-modal:before-close`** | Emits | **Yes** | Dispatched before closing. Calling `event.preventDefault()` cancels the close transition. |
101
+ | **`ln-modal:close`** | Emits | No | Dispatched after modal is closed, scroll locks released, and focus restored. |
102
+ | **`ln-modal:destroyed`** | Emits | No | Dispatched when the component instance is destroyed. |
103
+
104
+ ```js
105
+ // Example: Block close transition if form has unsaved changes
106
+ const modal = document.getElementById('user-modal');
107
+ modal.addEventListener('ln-modal:before-close', (e) => {
108
+ if (formIsDirty()) {
109
+ e.preventDefault(); // Cancels transition and reverts attribute to "open"
110
+ }
111
+ });
112
+ ```
113
+
114
+ ---
115
+
116
+ ## 5. Visual Sizing & SCSS Mixins
117
+
118
+ Do not use visual layout utility classes in your markup. Apply structural sizing variants inside your SCSS to the modal form element:
119
+
120
+ | Mixin | `max-width` | Ideal For |
121
+ |---|---|---|
122
+ | `@include modal-sm;` | `28rem` | Simple confirms, single-field inputs |
123
+ | `@include modal-md;` | `32rem` | Standard 2-4 field forms (Default) |
124
+ | `@include modal-lg;` | `42rem` | Wide multi-column forms, data lists |
125
+ | `@include modal-xl;` | `48rem` | Detail previews, large forms |
126
+
127
+ ```scss
128
+ // Apply in project SCSS
129
+ .ln-modal {
130
+ @include modal-overlay;
131
+
132
+ > form {
133
+ @include modal-panel;
134
+ @include modal-md; // default
135
+ }
136
+
137
+ &#user-modal > form {
138
+ @include modal-lg; // Override specific modal width
139
+ }
140
+ }
141
+ ```
142
+
143
+ ---
144
+
145
+ ## 6. Hash Addressing & Deep-Linking
146
+
147
+ Any modal with an `id` is hash-addressable (e.g. `#user-modal` or `#user-modal:42`).
148
+ When an `id` modal opens, `ln-modal` emits `ln-modal:open` containing `{ hashNs, param }` in `e.detail`.
149
+
150
+ Coordinators (such as `ln-modal-coordinator`) intercept hash changes and coordinate data fetching / form populating via `ln-fill`.
151
+
152
+ ---
153
+
154
+ ## 7. Related
155
+ - **[`ln-confirm`](../ln-confirm/README.md)** — Two-click inline confirm actions (lightweight alternative for single-element actions).
156
+ - **[`ln-form`](../ln-form/README.md)** — Form serialization and submission pipeline.
157
+ - **[`ln-validate`](../ln-validate/README.md)** — Declarative field-level constraints and visual errors.
158
+
159
+ ---
160
+
161
+ ## 🔧 Internals
162
+
163
+ Source: `js/ln-modal/src/ln-modal.js` (~137 lines, native `<dialog>`-backed). Trigger delegation (`data-ln-modal-for`), hash-addressing, and form-fill on open are NOT implemented here — they live in `js/ln-modal-coordinator/src/ln-modal-coordinator.js` (a Layer 2 coordinator per §1.4). This section covers `ln-modal.js` only.
164
+
165
+ ### Single source of truth
166
+
167
+ `data-ln-modal` is the only state. `_syncAttribute(el)` (wired via `registerComponent`'s `onAttributeChange`) is the sole place open/close side effects happen — there is no imperative `open()`/`close()` method on the instance. Cancellation is attribute-revert: if `ln-modal:before-open`/`before-close` is prevented, the handler writes the attribute back to its prior value, which the same no-op guard (`shouldBeOpen === instance.isOpen`) swallows on the resulting mutation.
168
+
169
+ ### Open / close
170
+
171
+ Open: cancelable `:before-open` → `instance.isOpen = true` → `body.ln-modal-open` class → `el.showModal()` (native `<dialog>` — top-layer stacking and backdrop are the platform's) → focus priority `[autofocus]` → first non-disabled input/select/textarea → first link/button, each filtered through `isVisible` → `:open`.
172
+
173
+ Close: cancelable `:before-close` → `instance.isOpen = false` → `:close` → `el.close()` (native — restores focus to the pre-open `document.activeElement` automatically) → if no other `[data-ln-modal="open"]` remains, remove `body.ln-modal-open`.
174
+
175
+ ### ESC handling
176
+
177
+ Not a custom keydown listener — the native `<dialog>` fires its own `cancel` event on ESC. `_onCancel` calls `preventDefault()` (so the dialog doesn't close ahead of the attribute-driven flow) and writes `data-ln-modal="close"`, routing ESC through the same single attribute path as every other close trigger.
178
+
179
+ ### Trigger/close wiring
180
+
181
+ The instance listens for `ln-modal:request-open`/`request-close` on the modal element itself (writes the attribute) and a `click` listener on the modal for `[data-ln-modal-close]` descendants. `data-ln-modal-for` trigger buttons are resolved by the coordinator's document-level delegated click listener, which looks up the modal by id and dispatches the request event — this file never sees the trigger element.
182
+
183
+ ### Destroy
184
+
185
+ Removes the four listeners (`request-open`, `request-close`, `cancel`, `click`), releases the body scroll-lock class if this was the last open modal, dispatches `:destroyed`, deletes the instance.
@@ -0,0 +1,13 @@
1
+ @use '../../scss/config/mixins' as *;
2
+
3
+ [data-ln-debug] {
4
+ // 1. Modal Trigger Check: [data-ln-modal-for] cannot have an empty target ID
5
+ [data-ln-modal-for=""] {
6
+ @include dev-inline-error("data-ln-modal-for is missing a target modal id");
7
+ }
8
+
9
+ // 2. Non-dialog modal: data-ln-modal must be on a <dialog> element (native showModal/close)
10
+ [data-ln-modal]:not(dialog) {
11
+ @include dev-inline-error("data-ln-modal must be on a <dialog> element");
12
+ }
13
+ }
@@ -1 +1 @@
1
- (function(){"use strict";function h(t,e,c){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:c||{}}))}function _(t,e,c){const m=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:c||{}});return t.dispatchEvent(m),m}function C(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){C(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function g(t,e,c,m){if(t.nodeType!==1)return;const b=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",n=Array.from(t.querySelectorAll(b));t.matches&&t.matches(b)&&n.push(t);for(const o of n)o[c]||(o[c]=new m(o))}function y(t){return!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)}function T(t,e,c,m,p={}){const b=p.extraAttributes||[],n=p.onAttributeChange||null,o=p.onInit||null;function r(u){const i=u||document.body;g(i,t,e,c),o&&o(i)}return C(function(){const u=new MutationObserver(function(a){for(let s=0;s<a.length;s++){const d=a[s];if(d.type==="childList"){for(let f=0;f<d.addedNodes.length;f++){const l=d.addedNodes[f];l.nodeType===1&&(g(l,t,e,c),o&&o(l))}for(let f=0;f<d.removedNodes.length;f++){const l=d.removedNodes[f];if(l.nodeType===1){const O=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",A=Array.from(l.querySelectorAll(O));l.matches&&l.matches(O)&&A.push(l);for(let E=0;E<A.length;E++){const x=A[E];if(!document.contains(x)){const w=x[e];w&&typeof w.destroy=="function"&&w.destroy()}}}}}else d.type==="attributes"&&(n&&d.target[e]?n(d.target,d.attributeName):(g(d.target,t,e,c),o&&o(d.target)))}});let i=[];if(t.indexOf("[")!==-1){const a=/\[([\w-]+)/g;let s;for(;(s=a.exec(t))!==null;)i.push(s[1])}else i.push(t);u.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:i.concat(b)})},m),window[e]=r,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){r(document.body)}):r(document.body),r}const L={};function k(t,e){L[t]=e}function S(t){return L[t]||{ingress:e=>e,egress:e=>e}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=k,window.lnCore.getDataMapper=S),(function(){const t="data-ln-modal",e="lnModal";if(window[e]!==void 0)return;function c(n){const o=Array.from(n.querySelectorAll("[data-ln-modal-for]"));n.hasAttribute&&n.hasAttribute("data-ln-modal-for")&&o.push(n);for(const r of o){if(r[e+"Trigger"])continue;const u=function(i){if(i.ctrlKey||i.metaKey||i.button===1)return;i.preventDefault();const a=r.getAttribute("data-ln-modal-for"),s=document.getElementById(a);if(!s){console.warn('[ln-modal] No modal found for data-ln-modal-for="'+a+'"');return}if(!s[e])return;const d=s.getAttribute(t);s.setAttribute(t,d==="open"?"close":"open")};r.addEventListener("click",u),r[e+"Trigger"]=u}}function m(n){this.dom=n,this.isOpen=n.getAttribute(t)==="open";const o=this;return this._onEscape=function(r){r.key==="Escape"&&o.dom.setAttribute(t,"close")},this._onFocusTrap=function(r){if(r.key!=="Tab")return;const u=Array.prototype.filter.call(o.dom.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'),y);if(u.length===0)return;const i=u[0],a=u[u.length-1];r.shiftKey?document.activeElement===i&&(r.preventDefault(),a.focus()):document.activeElement===a&&(r.preventDefault(),i.focus())},this._onClose=function(r){r.preventDefault(),o.dom.setAttribute(t,"close")},b(this),this.isOpen&&(this.dom.setAttribute("aria-modal","true"),this.dom.setAttribute("role","dialog"),document.body.classList.add("ln-modal-open"),document.addEventListener("keydown",this._onEscape),document.addEventListener("keydown",this._onFocusTrap)),this}m.prototype.destroy=function(){if(!this.dom[e])return;this.isOpen&&(this.dom.removeAttribute("aria-modal"),document.removeEventListener("keydown",this._onEscape),document.removeEventListener("keydown",this._onFocusTrap),this._returnFocusEl=null,document.querySelector("["+t+'="open"]')||document.body.classList.remove("ln-modal-open"));const n=this.dom.querySelectorAll("[data-ln-modal-close]");for(const r of n)r[e+"Close"]&&(r.removeEventListener("click",r[e+"Close"]),delete r[e+"Close"]);const o=document.querySelectorAll('[data-ln-modal-for="'+this.dom.id+'"]');for(const r of o)r[e+"Trigger"]&&(r.removeEventListener("click",r[e+"Trigger"]),delete r[e+"Trigger"]);h(this.dom,"ln-modal:destroyed",{modalId:this.dom.id,target:this.dom}),delete this.dom[e]};function p(n){const o=n[e];if(!o)return;const u=n.getAttribute(t)==="open";if(u!==o.isOpen)if(u){if(_(n,"ln-modal:before-open",{modalId:n.id,target:n}).defaultPrevented){n.setAttribute(t,"close");return}o.isOpen=!0,n.setAttribute("aria-modal","true"),n.setAttribute("role","dialog"),document.body.classList.add("ln-modal-open"),document.addEventListener("keydown",o._onEscape),document.addEventListener("keydown",o._onFocusTrap);const a=document.activeElement;o._returnFocusEl=a&&a!==document.body?a:null;const s=n.querySelector("[autofocus]");if(s&&y(s))s.focus();else{const d=n.querySelectorAll('input:not([disabled]):not([type="hidden"]), textarea:not([disabled]), select:not([disabled])'),f=Array.prototype.find.call(d,y);if(f)f.focus();else{const l=n.querySelectorAll("a[href], button:not([disabled])"),v=Array.prototype.find.call(l,y);v&&v.focus()}}h(n,"ln-modal:open",{modalId:n.id,target:n})}else{if(_(n,"ln-modal:before-close",{modalId:n.id,target:n}).defaultPrevented){n.setAttribute(t,"open");return}o.isOpen=!1,n.removeAttribute("aria-modal"),document.removeEventListener("keydown",o._onEscape),document.removeEventListener("keydown",o._onFocusTrap),h(n,"ln-modal:close",{modalId:n.id,target:n}),o._returnFocusEl&&document.contains(o._returnFocusEl)&&typeof o._returnFocusEl.focus=="function"&&o._returnFocusEl.focus(),o._returnFocusEl=null,document.querySelector("["+t+'="open"]')||document.body.classList.remove("ln-modal-open")}}function b(n){const o=n.dom.querySelectorAll("[data-ln-modal-close]");for(const r of o)r[e+"Close"]||(r.addEventListener("click",n._onClose),r[e+"Close"]=n._onClose)}T(t,e,m,"ln-modal",{extraAttributes:["data-ln-modal-for"],onAttributeChange:p,onInit:c})})()})();
1
+ (function(){"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 b={};function I(t,e){b[t]||(b[t]=document.querySelector('[data-ln-template="'+t+'"]'));const i=b[t];return i?i.content.cloneNode(!0):(console.warn("["+(e||"ln-core")+'] Template "'+t+'" not found'),null)}function y(t,e,i){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:i||{}}))}function O(t,e,i){const c=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:i||{}});return t.dispatchEvent(c),c}function x(t,e){if(!t||!e)return t;const i=t.querySelectorAll("[data-ln-field]");for(let o=0;o<i.length;o++){const s=i[o],r=s.getAttribute("data-ln-field");e[r]!=null&&(s.textContent=e[r])}const c=t.querySelectorAll("[data-ln-attr]");for(let o=0;o<c.length;o++){const s=c[o],r=s.getAttribute("data-ln-attr").split(",");for(let d=0;d<r.length;d++){const a=r[d].trim().split(":");if(a.length!==2)continue;const u=a[0].trim(),f=a[1].trim();e[f]!=null&&s.setAttribute(u,e[f])}}const n=t.querySelectorAll("[data-ln-show]");for(let o=0;o<n.length;o++){const s=n[o],r=s.getAttribute("data-ln-show");r in e&&s.classList.toggle("hidden",!e[r])}const l=t.querySelectorAll("[data-ln-class]");for(let o=0;o<l.length;o++){const s=l[o],r=s.getAttribute("data-ln-class").split(",");for(let d=0;d<r.length;d++){const a=r[d].trim().split(":");if(a.length!==2)continue;const u=a[0].trim(),f=a[1].trim();f in e&&s.classList.toggle(u,!!e[f])}}return t}function R(t,e){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));const i=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let c=0;c<i.length;c++)i[c].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)x(t.target,t.detail);else{const e=t.target.querySelectorAll("[data-ln-field]");for(let i=0;i<e.length;i++)e[i].textContent=""}})));function q(t,e){if(!t||!e)return t;const i=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;i.nextNode();){const l=i.currentNode;l.textContent.indexOf("{{")!==-1&&(l.textContent=l.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(o,s){return e[s]!==void 0?e[s]:""}))}const c=function(l,o){return e[o]!==void 0?e[o]:""},n=Array.from(t.querySelectorAll("*"));t.nodeType===1&&n.push(t);for(let l=0;l<n.length;l++){const o=n[l],s=o.attributes;for(let r=0;r<s.length;r++){const d=s[r];d.value.indexOf("{{")!==-1&&o.setAttribute(d.name,d.value.replace(/\{\{\s*(\w+)\s*\}\}/g,c))}}return t}function N(t,e,i,c,n,l){const o={};for(let r=0;r<t.children.length;r++){const d=t.children[r],a=d.getAttribute("data-ln-key");a&&(o[a]=d)}const s=document.createDocumentFragment();for(let r=0;r<e.length;r++){const d=e[r],a=String(c(d));let u=o[a];if(u)n(u,d,r);else{const f=I(i,l);if(!f||(q(f,d),u=f.firstElementChild,!u))continue;u.setAttribute("data-ln-key",a),n(u,d,r)}s.appendChild(u)}t.textContent="",t.appendChild(s)}function E(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){E(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function w(t,e,i,c){if(t.nodeType!==1)return;const l=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",o=Array.from(t.querySelectorAll(l));t.matches&&t.matches(l)&&o.push(t);for(const s of o)s[i]||(s[i]=new c(s))}function g(t){return!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)}function j(t,e,i,c,n={}){const l=n.extraAttributes||[],o=n.onAttributeChange||null,s=n.onInit||null;function r(d){const a=d||document.body;w(a,t,e,i),s&&s(a)}return E(function(){const d=new MutationObserver(function(u){for(let f=0;f<u.length;f++){const h=u[f];if(h.type==="childList"){for(let m=0;m<h.addedNodes.length;m++){const p=h.addedNodes[m];p.nodeType===1&&(w(p,t,e,i),s&&s(p))}for(let m=0;m<h.removedNodes.length;m++){const p=h.removedNodes[m];if(p.nodeType===1){const T=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",A=Array.from(p.querySelectorAll(T));p.matches&&p.matches(T)&&A.push(p);for(let v=0;v<A.length;v++){const D=A[v];if(!document.contains(D)){const L=D[e];L&&typeof L.destroy=="function"&&L.destroy()}}}}}else h.type==="attributes"&&(o&&h.target[e]?o(h.target,h.attributeName):(w(h.target,t,e,i),s&&s(h.target)))}});let a=[];if(t.indexOf("[")!==-1){const u=/\[([\w-]+)/g;let f;for(;(f=u.exec(t))!==null;)a.push(f[1])}else a.push(t);d.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:a.concat(l)})},c),window[e]=r,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){r(document.body)}):r(document.body),r}const _={};function F(t,e){_[t]=e}function W(t){return _[t]||{ingress:e=>e,egress:e=>e}}const S={};function k(t,e){if(!t||typeof e!="object")return;const i=t.toLowerCase().split("-")[0];S[i]=e}function B(t){if(!t)return null;const e=t.toLowerCase().split("-")[0];return S[e]||null}k("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=F,window.lnCore.getDataMapper=W,window.lnCore.registerLocaleFallback=k,window.lnCore.getLocaleFallback=B,window.lnCore.fillTemplate=q,window.lnCore.fill=x,window.lnCore.lnFill=R,window.lnCore.renderList=N);function M(t){return(t||"").replace(/^#/,"")}function C(t){const e=t===void 0?location.hash:t,i={},c=M(e);if(!c)return i;const n=c.split("&");for(let l=0;l<n.length;l++){const o=n[l];if(!o)continue;const s=o.indexOf(":"),r=s>-1?o.slice(0,s):o,d=s>-1?o.slice(s+1):"";if(r)try{i[r]=decodeURIComponent(d)}catch{i[r]=d}}return i}function K(t){if(!t)return null;const e=C();return t in e?e[t]:null}function P(t,e){if(!t)return;const i=C();e==null?delete i[t]:i[t]=String(e);const n=Object.keys(i).map(function(l){const o=i[l];return o===""?l:l+":"+encodeURIComponent(o)}).join("&");M(location.hash)!==n&&(location.hash=n)}function U(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=K,window.lnCore.hashSet=P,window.lnCore.hashLinkClick=U),(function(){const t="data-ln-modal",e="lnModal";if(window[e]!==void 0)return;function i(n){this.dom=n,this.isOpen=n.getAttribute(t)==="open";const l=this;return this._onRequestOpen=function(){l.dom.setAttribute(t,"open")},this._onRequestClose=function(){l.dom.setAttribute(t,"close")},this._onCancel=function(o){o.preventDefault(),l.dom.setAttribute(t,"close")},this._onClickClose=function(o){const s=o.target.closest("[data-ln-modal-close]");s&&l.dom.contains(s)&&(o.preventDefault(),l.dom.setAttribute(t,"close"))},this.dom.addEventListener("ln-modal:request-open",this._onRequestOpen),this.dom.addEventListener("ln-modal:request-close",this._onRequestClose),this.dom.addEventListener("cancel",this._onCancel),this.dom.addEventListener("click",this._onClickClose),this.isOpen&&(typeof this.dom.showModal=="function"&&this.dom.showModal(),document.body.classList.add("ln-modal-open")),this}i.prototype.destroy=function(){if(this.dom[e]){if(this.dom.removeEventListener("ln-modal:request-open",this._onRequestOpen),this.dom.removeEventListener("ln-modal:request-close",this._onRequestClose),this.dom.removeEventListener("cancel",this._onCancel),this.dom.removeEventListener("click",this._onClickClose),this.isOpen){const n=this.dom;Array.prototype.some.call(document.querySelectorAll("["+t+'="open"]'),function(o){return o!==n})||document.body.classList.remove("ln-modal-open")}y(this.dom,"ln-modal:destroyed",{modalId:this.dom.id,target:this.dom}),delete this.dom[e]}};function c(n){const l=n[e];if(!l)return;const s=n.getAttribute(t)==="open";if(s!==l.isOpen)if(s){if(O(n,"ln-modal:before-open",{modalId:n.id,target:n}).defaultPrevented){n.setAttribute(t,"close");return}l.isOpen=!0,document.body.classList.add("ln-modal-open"),typeof n.showModal=="function"&&n.showModal();const d=n.querySelector("[autofocus]");if(d&&g(d))d.focus();else{const a=n.querySelectorAll('input:not([disabled]):not([type="hidden"]), textarea:not([disabled]), select:not([disabled])'),u=Array.prototype.find.call(a,g);if(u)u.focus();else{const f=n.querySelectorAll("a[href], button:not([disabled])"),h=Array.prototype.find.call(f,g);h&&h.focus()}}y(n,"ln-modal:open",{modalId:n.id,target:n})}else{if(O(n,"ln-modal:before-close",{modalId:n.id,target:n}).defaultPrevented){n.setAttribute(t,"open");return}l.isOpen=!1,y(n,"ln-modal:close",{modalId:n.id,target:n}),typeof n.close=="function"&&n.close(),document.querySelector("["+t+'="open"]')||document.body.classList.remove("ln-modal-open")}}j(t,e,i,"ln-modal",{onAttributeChange:c})})()})();
@@ -1,11 +1,35 @@
1
1
  // ── JS state: modal open/close ──
2
- // data-ln-modal attribute drives visibility. JS sets "open"/"close".
3
- [data-ln-modal="open"] {
4
- display: flex;
5
- opacity: 1;
2
+ // data-ln-modal attribute drives visibility. JS calls native showModal()/close();
3
+ // this attribute is the CSS surface — reset native <dialog> UA styles and drive
4
+ // the flex-centering layout on top.
5
+ [data-ln-modal] {
6
+ background: transparent;
7
+ border: none;
8
+ padding: 0;
9
+ margin: 0;
10
+ width: 100%;
11
+ height: 100%;
12
+ max-width: none;
13
+ max-height: none;
14
+ color: inherit;
15
+ overflow: visible;
16
+
17
+ &[data-ln-modal="open"] {
18
+ display: flex;
19
+ }
6
20
  }
7
21
 
8
22
  // Prevent body scroll when modal is open
9
23
  body.ln-modal-open {
10
24
  overflow: hidden;
11
25
  }
26
+
27
+ // Mode-conditional title variants: shown only when ancestor modal matches the mode
28
+ [data-ln-modal-when] {
29
+ display: none;
30
+ }
31
+
32
+ [data-ln-modal-mode="new"] [data-ln-modal-when="new"],
33
+ [data-ln-modal-mode="edit"] [data-ln-modal-when="edit"] {
34
+ display: inline;
35
+ }