@livenetworks/ashlar 1.3.2

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 (232) hide show
  1. package/README.md +177 -0
  2. package/js/COMPONENTS.md +1102 -0
  3. package/js/index.js +41 -0
  4. package/js/ln-accordion/README.md +137 -0
  5. package/js/ln-accordion/ln-accordion.js +1 -0
  6. package/js/ln-accordion/src/ln-accordion.js +41 -0
  7. package/js/ln-ajax/README.md +91 -0
  8. package/js/ln-ajax/ln-ajax.js +1 -0
  9. package/js/ln-ajax/src/ln-ajax.js +277 -0
  10. package/js/ln-api-connector/README.md +150 -0
  11. package/js/ln-api-connector/ln-api-connector.js +1 -0
  12. package/js/ln-api-connector/src/ln-api-connector.js +265 -0
  13. package/js/ln-autoresize/README.md +80 -0
  14. package/js/ln-autoresize/ln-autoresize.js +1 -0
  15. package/js/ln-autoresize/src/ln-autoresize.js +47 -0
  16. package/js/ln-autosave/README.md +92 -0
  17. package/js/ln-autosave/ln-autosave.js +1 -0
  18. package/js/ln-autosave/src/ln-autosave.js +147 -0
  19. package/js/ln-circular-progress/README.md +161 -0
  20. package/js/ln-circular-progress/ln-circular-progress.js +1 -0
  21. package/js/ln-circular-progress/src/ln-circular-progress.js +133 -0
  22. package/js/ln-confirm/README.md +86 -0
  23. package/js/ln-confirm/_ln-confirm.scss +13 -0
  24. package/js/ln-confirm/ln-confirm.js +1 -0
  25. package/js/ln-confirm/src/ln-confirm.js +131 -0
  26. package/js/ln-core/crypto.js +83 -0
  27. package/js/ln-core/helpers.js +411 -0
  28. package/js/ln-core/index.js +5 -0
  29. package/js/ln-core/persist.js +71 -0
  30. package/js/ln-core/positioning.js +207 -0
  31. package/js/ln-core/reactive.js +74 -0
  32. package/js/ln-couchdb-connector/README.md +156 -0
  33. package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -0
  34. package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +348 -0
  35. package/js/ln-data-coordinator/README.md +165 -0
  36. package/js/ln-data-coordinator/ln-data-coordinator.js +1 -0
  37. package/js/ln-data-coordinator/src/ln-data-coordinator.js +249 -0
  38. package/js/ln-data-store/README.md +94 -0
  39. package/js/ln-data-store/ln-data-store.js +1 -0
  40. package/js/ln-data-store/src/ln-data-store.js +699 -0
  41. package/js/ln-data-table/README.md +110 -0
  42. package/js/ln-data-table/ln-data-table.js +1 -0
  43. package/js/ln-data-table/ln-data-table.scss +10 -0
  44. package/js/ln-data-table/src/ln-data-table.js +1103 -0
  45. package/js/ln-date/README.md +151 -0
  46. package/js/ln-date/ln-date.js +1 -0
  47. package/js/ln-date/src/ln-date.js +442 -0
  48. package/js/ln-dropdown/README.md +117 -0
  49. package/js/ln-dropdown/ln-dropdown.js +1 -0
  50. package/js/ln-dropdown/ln-dropdown.scss +15 -0
  51. package/js/ln-dropdown/src/ln-dropdown.js +174 -0
  52. package/js/ln-external-links/README.md +341 -0
  53. package/js/ln-external-links/ln-external-links.js +1 -0
  54. package/js/ln-external-links/src/ln-external-links.js +116 -0
  55. package/js/ln-filter/README.md +99 -0
  56. package/js/ln-filter/ln-filter.js +1 -0
  57. package/js/ln-filter/ln-filter.scss +7 -0
  58. package/js/ln-filter/src/ln-filter.js +404 -0
  59. package/js/ln-form/README.md +101 -0
  60. package/js/ln-form/ln-form.js +1 -0
  61. package/js/ln-form/src/ln-form.js +199 -0
  62. package/js/ln-http/README.md +89 -0
  63. package/js/ln-http/ln-http.js +1 -0
  64. package/js/ln-http/src/ln-http.js +219 -0
  65. package/js/ln-icons/README.md +88 -0
  66. package/js/ln-icons/ln-icons.js +1 -0
  67. package/js/ln-icons/src/ln-icons.js +169 -0
  68. package/js/ln-link/README.md +303 -0
  69. package/js/ln-link/ln-link.js +1 -0
  70. package/js/ln-link/src/ln-link.js +196 -0
  71. package/js/ln-modal/README.md +154 -0
  72. package/js/ln-modal/ln-modal.js +1 -0
  73. package/js/ln-modal/ln-modal.scss +11 -0
  74. package/js/ln-modal/src/ln-modal.js +201 -0
  75. package/js/ln-nav/README.md +70 -0
  76. package/js/ln-nav/ln-nav.js +1 -0
  77. package/js/ln-nav/src/ln-nav.js +177 -0
  78. package/js/ln-number/README.md +122 -0
  79. package/js/ln-number/ln-number.js +1 -0
  80. package/js/ln-number/src/ln-number.js +302 -0
  81. package/js/ln-popover/README.md +127 -0
  82. package/js/ln-popover/ln-popover.js +1 -0
  83. package/js/ln-popover/src/ln-popover.js +288 -0
  84. package/js/ln-progress/README.md +442 -0
  85. package/js/ln-progress/ln-progress.js +1 -0
  86. package/js/ln-progress/src/ln-progress.js +150 -0
  87. package/js/ln-search/README.md +83 -0
  88. package/js/ln-search/ln-search.js +1 -0
  89. package/js/ln-search/ln-search.scss +7 -0
  90. package/js/ln-search/src/ln-search.js +114 -0
  91. package/js/ln-sortable/README.md +95 -0
  92. package/js/ln-sortable/ln-sortable.js +1 -0
  93. package/js/ln-sortable/src/ln-sortable.js +203 -0
  94. package/js/ln-table/README.md +101 -0
  95. package/js/ln-table/ln-table-sort.js +1 -0
  96. package/js/ln-table/ln-table.js +1 -0
  97. package/js/ln-table/ln-table.scss +11 -0
  98. package/js/ln-table/src/ln-table-sort.js +168 -0
  99. package/js/ln-table/src/ln-table.js +473 -0
  100. package/js/ln-tabs/README.md +137 -0
  101. package/js/ln-tabs/ln-tabs.js +1 -0
  102. package/js/ln-tabs/src/ln-tabs.js +171 -0
  103. package/js/ln-time/README.md +81 -0
  104. package/js/ln-time/ln-time.js +1 -0
  105. package/js/ln-time/src/ln-time.js +192 -0
  106. package/js/ln-toast/README.md +122 -0
  107. package/js/ln-toast/ln-toast.js +15 -0
  108. package/js/ln-toast/src/ln-toast.js +210 -0
  109. package/js/ln-toast/template.html +14 -0
  110. package/js/ln-toggle/README.md +137 -0
  111. package/js/ln-toggle/ln-toggle.js +1 -0
  112. package/js/ln-toggle/src/ln-toggle.js +139 -0
  113. package/js/ln-tooltip/README.md +58 -0
  114. package/js/ln-tooltip/ln-tooltip.js +1 -0
  115. package/js/ln-tooltip/ln-tooltip.scss +9 -0
  116. package/js/ln-tooltip/src/ln-tooltip.js +169 -0
  117. package/js/ln-translations/README.md +96 -0
  118. package/js/ln-translations/ln-translations.js +1 -0
  119. package/js/ln-translations/src/ln-translations.js +275 -0
  120. package/js/ln-upload/README.md +180 -0
  121. package/js/ln-upload/ln-upload.js +1 -0
  122. package/js/ln-upload/ln-upload.scss +20 -0
  123. package/js/ln-upload/src/ln-upload.js +407 -0
  124. package/js/ln-validate/README.md +108 -0
  125. package/js/ln-validate/ln-validate.js +1 -0
  126. package/js/ln-validate/src/ln-validate.js +160 -0
  127. package/package.json +55 -0
  128. package/scss/base/_global.scss +83 -0
  129. package/scss/base/_reset.scss +17 -0
  130. package/scss/base/_typography.scss +125 -0
  131. package/scss/components/_accordion.scss +34 -0
  132. package/scss/components/_ajax.scss +15 -0
  133. package/scss/components/_alert.scss +5 -0
  134. package/scss/components/_app-shell.scss +15 -0
  135. package/scss/components/_avatar.scss +6 -0
  136. package/scss/components/_breadcrumbs.scss +33 -0
  137. package/scss/components/_button.scss +20 -0
  138. package/scss/components/_card.scss +10 -0
  139. package/scss/components/_chip.scss +5 -0
  140. package/scss/components/_circular-progress.scss +29 -0
  141. package/scss/components/_confirm.scss +5 -0
  142. package/scss/components/_data-table.scss +83 -0
  143. package/scss/components/_dropdown.scss +25 -0
  144. package/scss/components/_empty-state.scss +22 -0
  145. package/scss/components/_form.scss +100 -0
  146. package/scss/components/_layout.scss +8 -0
  147. package/scss/components/_link.scss +11 -0
  148. package/scss/components/_ln-table.scss +60 -0
  149. package/scss/components/_loader.scss +6 -0
  150. package/scss/components/_modal.scss +20 -0
  151. package/scss/components/_nav.scss +9 -0
  152. package/scss/components/_page-header.scss +10 -0
  153. package/scss/components/_popover.scss +10 -0
  154. package/scss/components/_progress.scss +17 -0
  155. package/scss/components/_prose.scss +5 -0
  156. package/scss/components/_scrollbar.scss +32 -0
  157. package/scss/components/_sections.scss +12 -0
  158. package/scss/components/_sidebar.scss +5 -0
  159. package/scss/components/_stat-card.scss +5 -0
  160. package/scss/components/_status-badge.scss +4 -0
  161. package/scss/components/_stepper.scss +5 -0
  162. package/scss/components/_table.scss +19 -0
  163. package/scss/components/_tabs.scss +21 -0
  164. package/scss/components/_timeline.scss +14 -0
  165. package/scss/components/_toast.scss +41 -0
  166. package/scss/components/_toggle.scss +81 -0
  167. package/scss/components/_tooltip.scss +18 -0
  168. package/scss/components/_translations.scss +111 -0
  169. package/scss/components/_upload.scss +51 -0
  170. package/scss/config/_breakpoints.scss +72 -0
  171. package/scss/config/_density.scss +117 -0
  172. package/scss/config/_icons.scss +37 -0
  173. package/scss/config/_mixins.scss +13 -0
  174. package/scss/config/_theme.scss +216 -0
  175. package/scss/config/_tokens.scss +419 -0
  176. package/scss/config/mixins/_accordion.scss +52 -0
  177. package/scss/config/mixins/_ajax.scss +39 -0
  178. package/scss/config/mixins/_alert.scss +82 -0
  179. package/scss/config/mixins/_app-shell.scss +312 -0
  180. package/scss/config/mixins/_avatar.scss +109 -0
  181. package/scss/config/mixins/_borders.scss +36 -0
  182. package/scss/config/mixins/_breadcrumbs.scss +72 -0
  183. package/scss/config/mixins/_breakpoints.scss +62 -0
  184. package/scss/config/mixins/_btn.scss +179 -0
  185. package/scss/config/mixins/_card.scss +338 -0
  186. package/scss/config/mixins/_chip.scss +66 -0
  187. package/scss/config/mixins/_circular-progress.scss +71 -0
  188. package/scss/config/mixins/_collapsible.scss +24 -0
  189. package/scss/config/mixins/_colors.scss +46 -0
  190. package/scss/config/mixins/_confirm.scss +31 -0
  191. package/scss/config/mixins/_data-table.scss +346 -0
  192. package/scss/config/mixins/_display.scss +32 -0
  193. package/scss/config/mixins/_dropdown.scss +143 -0
  194. package/scss/config/mixins/_empty-state.scss +30 -0
  195. package/scss/config/mixins/_focus.scss +55 -0
  196. package/scss/config/mixins/_footer.scss +42 -0
  197. package/scss/config/mixins/_form.scss +601 -0
  198. package/scss/config/mixins/_index.scss +58 -0
  199. package/scss/config/mixins/_interaction.scss +15 -0
  200. package/scss/config/mixins/_kbd.scss +22 -0
  201. package/scss/config/mixins/_layout.scss +117 -0
  202. package/scss/config/mixins/_link.scss +55 -0
  203. package/scss/config/mixins/_ln-table.scss +420 -0
  204. package/scss/config/mixins/_loader.scss +26 -0
  205. package/scss/config/mixins/_modal.scss +66 -0
  206. package/scss/config/mixins/_motion.scss +19 -0
  207. package/scss/config/mixins/_nav.scss +273 -0
  208. package/scss/config/mixins/_page-header.scss +69 -0
  209. package/scss/config/mixins/_popover.scss +25 -0
  210. package/scss/config/mixins/_position.scss +32 -0
  211. package/scss/config/mixins/_progress.scss +56 -0
  212. package/scss/config/mixins/_prose.scss +127 -0
  213. package/scss/config/mixins/_shadows.scss +8 -0
  214. package/scss/config/mixins/_sidebar.scss +95 -0
  215. package/scss/config/mixins/_sizing.scss +6 -0
  216. package/scss/config/mixins/_spacing.scss +19 -0
  217. package/scss/config/mixins/_stat-card.scss +68 -0
  218. package/scss/config/mixins/_status-badge.scss +83 -0
  219. package/scss/config/mixins/_stepper.scss +78 -0
  220. package/scss/config/mixins/_table.scss +215 -0
  221. package/scss/config/mixins/_tabs.scss +64 -0
  222. package/scss/config/mixins/_timeline.scss +69 -0
  223. package/scss/config/mixins/_toast.scss +148 -0
  224. package/scss/config/mixins/_tooltip.scss +111 -0
  225. package/scss/config/mixins/_transitions.scss +10 -0
  226. package/scss/config/mixins/_translations.scss +124 -0
  227. package/scss/config/mixins/_typography.scss +57 -0
  228. package/scss/config/mixins/_upload.scss +168 -0
  229. package/scss/ln-ashlar.scss +62 -0
  230. package/scss/tabler-icons.txt +5039 -0
  231. package/scss/utilities/_animations.scss +83 -0
  232. package/scss/utilities/_utilities.scss +49 -0
@@ -0,0 +1,442 @@
1
+ # ln-progress
2
+
3
+ > A passive linear-progress renderer. Author writes
4
+ > `data-ln-progress="42"` on a bar element; the component picks up the
5
+ > change via MutationObserver and writes the new `width` as a
6
+ > percentage. 145 lines of JS.
7
+
8
+ ## Integration
9
+
10
+ ### In-Bundle (Standard Integration)
11
+ To load `ln-progress` as part of the unified `ln-ashlar` bundle, include the main script:
12
+ ```html
13
+ <script src="dist/ln-ashlar.iife.js" defer></script>
14
+ ```
15
+
16
+ ### Standalone (Zero-Dependency IIFE)
17
+ If you only need the progress bar component, load the compiled zero-dependency IIFE directly:
18
+ ```html
19
+ <script src="js/ln-progress/ln-progress.js" defer></script>
20
+ ```
21
+
22
+ ### Source Files & Development
23
+ - **Active Development Source**: [js/ln-progress/src/ln-progress.js](file:///c:/laragon/www/ln-ashlar/js/ln-progress/src/ln-progress.js) — The source of truth for component logic.
24
+ - **Compiled Standalone**: [js/ln-progress/ln-progress.js](file:///c:/laragon/www/ln-ashlar/js/ln-progress/ln-progress.js) — The compiled, ready-to-use standalone bundle.
25
+
26
+ ## Philosophy
27
+
28
+ A linear progress indicator is mechanically a track `<div>` wrapping
29
+ one or more child bar `<div>`s whose `width` is a percentage of the
30
+ track. The maths is `width = (value / max) * 100%`. What `ln-progress`
31
+ adds is the reactive plumbing: when the width gets recomputed, what
32
+ triggers a re-render, where the maximum comes from when multiple bars
33
+ share a denominator, and what survives a `destroy()` call.
34
+
35
+ The contract is simple: **the attribute IS the state**. There is no
36
+ `element.setValue(42)` API and no imperative entry point. You set
37
+ `data-ln-progress="42"` on a bar; the MutationObserver sees the
38
+ change; `_render` runs; the bar redraws. State is visible in DOM
39
+ Inspector at all times, can be set declaratively from server templates,
40
+ and survives `outerHTML` round-trips.
41
+
42
+ The component supports stacked bars sharing one denominator: declare
43
+ `data-ln-progress-max` on the parent track and every child bar reads
44
+ that as its max. This is what makes "4 done + 2 in progress + 1
45
+ pending = 7 total" render as three bars filling exactly one track.
46
+
47
+ ## Quick start
48
+
49
+ ```html
50
+ <div class="progress">
51
+ <div data-ln-progress="75" class="success"></div>
52
+ </div>
53
+ ```
54
+
55
+ A green bar at 75% of the track, no JS on the consumer side. The
56
+ `class="progress"` on the wrapper marks it as a track (pure CSS hook,
57
+ no JS instance is created). The `data-ln-progress="75"` on the child
58
+ marks it as a bar (instance is created, `style.width = "75%"` is
59
+ written).
60
+
61
+ ## Attributes
62
+
63
+ | Attribute | On | Description |
64
+ |---|---|---|
65
+ | `data-ln-progress="N"` | bar element | Current value of the bar. Number; can be negative or above max — the rendered percentage clamps to 0–100. Empty / non-numeric values render as 0%. |
66
+ | `data-ln-progress-max="N"` | bar element | Maximum value for this bar. Default `100`. Used as the denominator. Setting `max="0"` collapses to 0%. |
67
+ | `data-ln-progress-max="N"` | track element | Shared max for all bar children. Wins over each bar's own max. The track itself uses `class="progress"` for styling — no JS instance is created on it. |
68
+ | `class="success"` / `.warning` / `.error` | bar element | Colour variant. Plain CSS, no JS involvement. |
69
+
70
+ The track is marked with `class="progress"` (pure CSS hook). The bar
71
+ is marked with `data-ln-progress="N"` (JS instance).
72
+
73
+ The component writes `role="progressbar"`, `aria-valuemin="0"`,
74
+ `aria-valuemax`, and `aria-valuenow` on **each bar element** on every
75
+ render. `aria-valuenow` is clamped to `[0, max]` to match the rendered
76
+ bar (so an overshoot value of `150` against `max=100` reports as
77
+ `100`). Add `aria-label` on the bar yourself for screen-reader context
78
+ — the component does not invent a label.
79
+
80
+ ## Events
81
+
82
+ One event, bubbles, not cancelable.
83
+
84
+ | Event | `detail` | Dispatched on | Dispatched when |
85
+ |---|---|---|---|
86
+ | `ln-progress:change` | `{ target: HTMLElement, value: number, max: number, percentage: number }` | the bar element | every `_render` call: at construction, on every value or max attribute change (bar's own value, bar's own max, or parent track's max if observed) |
87
+
88
+ `detail.value` is the raw `parseFloat` of the value attribute —
89
+ unclamped. `detail.max` is the resolved max (parent's first, then
90
+ bar's own, then 100). `detail.percentage` is the clamped 0–100
91
+ percentage written to `style.width`.
92
+
93
+ The event fires once per attribute write. Writing
94
+ `data-ln-progress="50"` then `"75"` synchronously fires the event
95
+ twice. The CSS transition smooths the visual to a single sweep, but
96
+ the event count is per-write.
97
+
98
+ There is no `:initialized` and no `:destroyed` event. The first
99
+ `:change` fires inside `_render` during construction and serves as
100
+ the init signal for any consumer listening at the document level.
101
+
102
+ ## API
103
+
104
+ `window.lnProgress(root)` re-runs the init scan over `root`. The
105
+ document-level observer already covers AJAX inserts and
106
+ `data-ln-progress` attribute additions; call this manually only when
107
+ you inject markup into a Shadow DOM root or another document context
108
+ the observer cannot see.
109
+
110
+ `el.lnProgress` on a bar exposes:
111
+
112
+ | Property | Type | Description |
113
+ |---|---|---|
114
+ | `dom` | `HTMLElement` | back-reference to the bar element |
115
+ | `_attrObserver` | `MutationObserver` | watches the bar's own `data-ln-progress` and `data-ln-progress-max` |
116
+ | `_parentObserver` | `MutationObserver \| null` | watches the parent track's `data-ln-progress-max` if the parent had that attribute at construction; `null` otherwise |
117
+ | `destroy()` | method | disconnects both observers, deletes `el.lnProgress`. Does NOT remove `data-ln-progress` itself, the colour class, or the inline `style.width`. |
118
+
119
+ There is no `setValue(n)`, `setMax(n)`, `update()`, or `redraw()`
120
+ method. The attribute is the API. A programmatic update is
121
+ `el.setAttribute('data-ln-progress', n)`, exactly the same as a
122
+ server-side render that writes the attribute into markup. To force a
123
+ re-render without changing the value, write the same value back —
124
+ the MutationObserver fires on any write, including identical ones.
125
+
126
+ ## Max priority resolution
127
+
128
+ `_render` resolves the maximum in this order:
129
+
130
+ | Priority | Source | Truthy check |
131
+ |---|---|---|
132
+ | 1 | parent track's `data-ln-progress-max` parsed as float | parses to a finite number > 0 |
133
+ | 2 | bar's own `data-ln-progress-max` parsed as float | parses to a finite number > 0 |
134
+ | 3 | `100` (default) | always |
135
+
136
+ The chain is `parentMax || ownMax || 100`. Two consequences:
137
+
138
+ 1. A parent declaring `data-ln-progress-max="0"` falls through to the
139
+ bar's own max, because `0` is falsy in JS. Same for non-numeric
140
+ values like `data-ln-progress-max="abc"` — `NaN` is falsy.
141
+ 2. An own-max declaration is silently ignored when the parent declares
142
+ one. If a bar carries `data-ln-progress-max="50"` and the parent
143
+ carries `data-ln-progress-max="100"`, the parent wins — the bar
144
+ measures against 100. If you genuinely need per-bar maxes inside a
145
+ shared-max parent, do not declare the parent max; set each bar's
146
+ own max individually and accept that the bars no longer fill the
147
+ track in proportional fashion.
148
+
149
+ ## Examples
150
+
151
+ ### Minimal — fixed value
152
+
153
+ ```html
154
+ <div class="progress">
155
+ <div data-ln-progress="75" class="success"></div>
156
+ </div>
157
+ ```
158
+
159
+ The simplest case. Green bar at 75%, no JS on the consumer side.
160
+
161
+ ### Reactive — change value at runtime
162
+
163
+ ```html
164
+ <div id="upload-track" class="progress">
165
+ <div id="upload-bar" data-ln-progress="0" class="success"
166
+ aria-label="Upload progress"></div>
167
+ </div>
168
+ <button id="advance">Advance</button>
169
+ ```
170
+
171
+ ```js
172
+ let pct = 0;
173
+ document.getElementById('advance').addEventListener('click', function () {
174
+ pct = Math.min(100, pct + 10);
175
+ document.getElementById('upload-bar').setAttribute('data-ln-progress', pct);
176
+ });
177
+ ```
178
+
179
+ Each attribute write triggers a re-render and a `:change` event. The
180
+ CSS transition smoothly animates between widths. The component writes
181
+ `aria-valuenow` on the bar automatically — no manual sync needed.
182
+
183
+ ### Custom max — non-percentage scale (per-bar)
184
+
185
+ ```html
186
+ <!-- 7.5 GB of 10 GB — bar fills 75% of the track -->
187
+ <div class="progress">
188
+ <div data-ln-progress="7.5" data-ln-progress-max="10" class="warning"></div>
189
+ </div>
190
+ ```
191
+
192
+ `max="10"` shifts the denominator so 7.5 maps to 75%. There is no
193
+ text label slot inside the bar; place descriptive text as a sibling
194
+ element if you need it.
195
+
196
+ ### Stacked — multiple bars, one track, default max=100
197
+
198
+ ```html
199
+ <!-- CPU 40% / RAM 30% / Disk 30% — sums to 100% of the track -->
200
+ <div class="progress">
201
+ <div data-ln-progress="40" class="success" aria-label="CPU usage"></div>
202
+ <div data-ln-progress="30" class="warning" aria-label="RAM usage"></div>
203
+ <div data-ln-progress="30" class="error" aria-label="Disk usage"></div>
204
+ </div>
205
+ ```
206
+
207
+ Each bar uses the default max of 100. Three bars at 40 + 30 + 30 fill
208
+ the track exactly. If the values sum to less than 100, the remainder
209
+ of the track stays empty (the track's recessed background shows
210
+ through). If more than 100, each bar still renders at its clamped
211
+ 0–100 width and the visuals overflow the track horizontally — the
212
+ SCSS `overflow: hidden` clips the overflow so the visual stays clean,
213
+ but the maths is wrong.
214
+
215
+ ### Stacked — shared max on the parent track
216
+
217
+ ```html
218
+ <!-- 4 done + 2 in progress + 1 pending = 7 total — fills the track exactly -->
219
+ <div class="progress" data-ln-progress-max="7">
220
+ <div data-ln-progress="4" class="success" aria-label="Done"></div>
221
+ <div data-ln-progress="2" class="warning" aria-label="In progress"></div>
222
+ <div data-ln-progress="1" class="error" aria-label="Pending"></div>
223
+ </div>
224
+ ```
225
+
226
+ The track carries `data-ln-progress-max="7"`. Each child bar's
227
+ `_render` reads the parent's max first and uses `7` as the
228
+ denominator. Bar widths are 4/7 ≈ 57.1%, 2/7 ≈ 28.6%, 1/7 ≈ 14.3%,
229
+ summing to 100%.
230
+
231
+ Changing the parent's max at runtime
232
+ (`track.setAttribute('data-ln-progress-max', '10')`) re-renders each
233
+ child via the per-bar `_parentObserver` — but only for bars that
234
+ were constructed when the parent already had the attribute. See
235
+ "Common mistakes" item 4.
236
+
237
+ ### Upload progress — driving from XHR
238
+
239
+ ```html
240
+ <form id="upload-form">
241
+ <input type="file" name="file">
242
+ <div id="upload-track" class="progress">
243
+ <div id="upload-bar" data-ln-progress="0" class="success"
244
+ aria-label="Upload progress"></div>
245
+ </div>
246
+ <button type="submit">Upload</button>
247
+ </form>
248
+ ```
249
+
250
+ ```js
251
+ document.getElementById('upload-form').addEventListener('submit', function (e) {
252
+ e.preventDefault();
253
+ const bar = document.getElementById('upload-bar');
254
+ const file = e.target.querySelector('input[type=file]').files[0];
255
+ if (!file) return;
256
+
257
+ const xhr = new XMLHttpRequest();
258
+ const fd = new FormData(e.target);
259
+
260
+ xhr.upload.addEventListener('progress', function (ev) {
261
+ if (!ev.lengthComputable) return;
262
+ const pct = Math.round((ev.loaded / ev.total) * 100);
263
+ bar.setAttribute('data-ln-progress', pct);
264
+ });
265
+
266
+ xhr.upload.addEventListener('load', function () {
267
+ bar.setAttribute('data-ln-progress', 100);
268
+ });
269
+
270
+ xhr.open('POST', '/upload');
271
+ xhr.send(fd);
272
+ });
273
+ ```
274
+
275
+ ARIA on the bar is managed automatically by `ln-progress` —
276
+ `aria-valuenow` updates on every attribute write. The `progress`
277
+ event fires often during a large upload — every 16ms or so on a fast
278
+ connection. Each call rewrites the attribute, the MutationObserver
279
+ fires, `_render` runs, and the `:change` event dispatches. If
280
+ profiling shows it as a hot spot, throttle the JS-side write to
281
+ every 100ms before calling `setAttribute`.
282
+
283
+ ### Threshold-based colour — consumer code
284
+
285
+ The component has no automatic threshold colours. `class="success"`
286
+ / `.warning` / `.error` are static — JS does not flip them based on
287
+ value. If you want red below 30% and green above 70%, that is
288
+ consumer code:
289
+
290
+ ```js
291
+ function setProgressWithThreshold(bar, value) {
292
+ bar.setAttribute('data-ln-progress', value);
293
+ bar.classList.remove('success', 'warning', 'error');
294
+ if (value < 30) bar.classList.add('error');
295
+ else if (value < 70) bar.classList.add('warning');
296
+ else bar.classList.add('success');
297
+ }
298
+ ```
299
+
300
+ The colour-class swap is a `classList` write — no MutationObserver
301
+ fires for it (the component watches `data-ln-progress` and
302
+ `data-ln-progress-max`, not `class`). The new colour applies via plain
303
+ CSS rules in `scss/components/_progress.scss`.
304
+
305
+ ## What `ln-progress` does NOT do
306
+
307
+ - **Does NOT watch the parent for late-added max.** `_listenParent`
308
+ runs once at construction and bails immediately if the parent does
309
+ not currently have `data-ln-progress-max`. If the parent gains the
310
+ max later, the bar will not re-render in response. See "Common
311
+ mistakes" item 4.
312
+ - **Does NOT clamp the input attribute.** It clamps the *computed
313
+ percentage* between 0 and 100 before writing the width. The
314
+ attribute can hold any number; reading
315
+ `el.getAttribute('data-ln-progress')` after writing `'150'` returns
316
+ `'150'`, not `'100'`.
317
+ - **Does NOT have an indeterminate / striped / pulsing mode.**
318
+ No animated-stripes overlay, no shimmer, no pulse. For an
319
+ indeterminate indicator on an unbounded operation, use
320
+ `@mixin loader` (`scss/config/mixins/_loader.scss`) — different
321
+ visual idiom, separate primitive.
322
+ - **Does NOT debounce or throttle rapid-fire value changes.** Every
323
+ attribute write triggers a synchronous `_render` call. For
324
+ high-frequency flows (60+ writes/sec), the consumer is responsible
325
+ for throttling.
326
+
327
+ ## Common mistakes
328
+
329
+ ### 1. Setting `style.width` manually inside the markup
330
+
331
+ ```html
332
+ <!-- WRONG — JS overwrites style.width on construction -->
333
+ <div class="progress">
334
+ <div data-ln-progress="75" class="success" style="width: 50%"></div>
335
+ </div>
336
+ ```
337
+
338
+ The constructor's first action is `_render`, which writes
339
+ `this.dom.style.width = percentage + '%'`. Whatever inline width you
340
+ set in markup is overwritten on the first paint. If you want the bar
341
+ to render at a specific width, set the value via `data-ln-progress`,
342
+ not `style.width`.
343
+
344
+ ### 2. Mixing percentage values with absolute max
345
+
346
+ ```html
347
+ <!-- WRONG — value is "50" interpreted as a count, not 50% -->
348
+ <div class="progress">
349
+ <div data-ln-progress="50" data-ln-progress-max="500" class="success"></div>
350
+ </div>
351
+ ```
352
+
353
+ This renders 50/500 = 10% of the track, probably not what the author
354
+ meant. The component does not interpret the value as a percentage; it
355
+ treats it as a count against the max. Either write the absolute count
356
+ (`250` for "50% of 500") or drop the max and use the default of 100
357
+ with the percentage as the value.
358
+
359
+ ### 3. Multiple bars sharing a parent without declaring parent-max
360
+
361
+ ```html
362
+ <!-- WRONG — three bars at 4 / 2 / 1, default max=100 each → 7% of track filled -->
363
+ <div class="progress">
364
+ <div data-ln-progress="4" class="success"></div>
365
+ <div data-ln-progress="2" class="warning"></div>
366
+ <div data-ln-progress="1" class="error"></div>
367
+ </div>
368
+ ```
369
+
370
+ If the intent was "4 + 2 + 1 = 7 items, all visible proportionally,"
371
+ declare the parent's max:
372
+
373
+ ```html
374
+ <!-- RIGHT -->
375
+ <div class="progress" data-ln-progress-max="7">
376
+ <div data-ln-progress="4" class="success"></div>
377
+ <div data-ln-progress="2" class="warning"></div>
378
+ <div data-ln-progress="1" class="error"></div>
379
+ </div>
380
+ ```
381
+
382
+ Each bar reads parent max (`7`) as the denominator, and 4/7 + 2/7 +
383
+ 1/7 fills the track exactly.
384
+
385
+ ### 4. Setting `data-ln-progress-max` on the parent AFTER construction
386
+
387
+ ```js
388
+ // WRONG — bars constructed without parent max, _listenParent never attached
389
+ const track = document.getElementById('my-track');
390
+ track.setAttribute('data-ln-progress-max', '7');
391
+ // → bars do not re-render in response to this write
392
+ ```
393
+
394
+ `_listenParent` runs once at construction and bails immediately if
395
+ the parent did not declare `data-ln-progress-max` at that moment.
396
+ The parent observer is conditional, not lazy. If the parent gains
397
+ the attribute later, no per-bar observer exists to catch it.
398
+
399
+ Three resolutions:
400
+
401
+ ```js
402
+ // Option A — declare parent max in markup so it exists at construction
403
+ <div class="progress" data-ln-progress-max="7"> ... </div>
404
+
405
+ // Option B — after setting the parent max, force each bar to re-render by
406
+ // rewriting one of its own observed attributes
407
+ track.setAttribute('data-ln-progress-max', '7');
408
+ track.querySelectorAll('[data-ln-progress]').forEach(function (bar) {
409
+ bar.setAttribute('data-ln-progress', bar.getAttribute('data-ln-progress'));
410
+ // → bar's own _attrObserver fires → _render re-resolves max from scratch
411
+ });
412
+
413
+ // Option C — destroy and re-init
414
+ track.querySelectorAll('[data-ln-progress]').forEach(function (bar) {
415
+ if (bar.lnProgress) bar.lnProgress.destroy();
416
+ });
417
+ window.lnProgress(track);
418
+ ```
419
+
420
+ Option A is canonical. Option B is the right runtime workaround when
421
+ the markup was static-with-default-max and you need to switch to
422
+ shared-max dynamically.
423
+
424
+ ### 5. Forgetting `class="progress"` on the track wrapper
425
+
426
+ Without the class, the wrapper has no styling — no recessed
427
+ background, no rounded edges, no overflow clip. The bar still
428
+ renders correctly but visually escapes its track.
429
+
430
+ ## Related
431
+
432
+ - **`@mixin progress`** (`scss/config/mixins/_progress.scss`) — the
433
+ recipe. Defines the track height, the recessed background, the
434
+ rounded full radius, the overflow clip, and the inner-bar rules
435
+ (initial `width: 0`, `transition: width var(--transition-base)`
436
+ gated through `motion-safe`, rounded right edge on the last child).
437
+ - **`scss/components/_progress.scss`** — applies the base mixin to
438
+ `.progress` and applies the `.success` / `.warning` /
439
+ `.error` colour variants.
440
+ - **Architecture deep-dive:** [`docs/js/progress.md`](../../docs/js/progress.md)
441
+ for the construction flow, the three observers, the max-priority
442
+ resolution, and the late-parent-max edge case.
@@ -0,0 +1 @@
1
+ (function(){"use strict";function b(a,t,o){a.dispatchEvent(new CustomEvent(t,{bubbles:!0,detail:o||{}}))}function c(a,t){if(!document.body){document.addEventListener("DOMContentLoaded",function(){c(a,t)}),console.warn("["+t+'] Script loaded before <body> — add "defer" to your <script> tag');return}a()}const f={};function p(a,t){f[a]=t}function m(a){return f[a]||{ingress:t=>t,egress:t=>t}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=p,window.lnCore.getDataMapper=m),(function(){const a="[data-ln-progress]",t="lnProgress";if(window[t]!==void 0)return;function o(e){u(e)}function u(e){const r=Array.from(e.querySelectorAll(a));for(const n of r)n[t]||(n[t]=new d(n));e.hasAttribute&&e.hasAttribute("data-ln-progress")&&!e[t]&&(e[t]=new d(e))}function d(e){return this.dom=e,this._attrObserver=null,this._parentObserver=null,l.call(this),g.call(this),v.call(this),this}d.prototype.destroy=function(){this.dom[t]&&(this._attrObserver&&this._attrObserver.disconnect(),this._parentObserver&&this._parentObserver.disconnect(),delete this.dom[t])};function h(){c(function(){new MutationObserver(function(r){for(const n of r)if(n.type==="childList")for(const s of n.addedNodes)s.nodeType===1&&u(s);else n.type==="attributes"&&u(n.target)}).observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["data-ln-progress"]})},"ln-progress")}h();function g(){const e=this,r=new MutationObserver(function(n){for(const s of n)(s.attributeName==="data-ln-progress"||s.attributeName==="data-ln-progress-max")&&l.call(e)});r.observe(this.dom,{attributes:!0,attributeFilter:["data-ln-progress","data-ln-progress-max"]}),this._attrObserver=r}function v(){const e=this,r=this.dom.parentElement;if(!r||!r.hasAttribute("data-ln-progress-max"))return;const n=new MutationObserver(function(s){for(const i of s)i.attributeName==="data-ln-progress-max"&&l.call(e)});n.observe(r,{attributes:!0,attributeFilter:["data-ln-progress-max"]}),this._parentObserver=n}function l(){const e=parseFloat(this.dom.getAttribute("data-ln-progress"))||0,r=this.dom.parentElement,s=(r&&r.hasAttribute("data-ln-progress-max")?parseFloat(r.getAttribute("data-ln-progress-max")):null)||parseFloat(this.dom.getAttribute("data-ln-progress-max"))||100;let i=s>0?e/s*100:0;i<0&&(i=0),i>100&&(i=100),this.dom.style.width=i+"%";const w=Math.max(0,Math.min(e,s));this.dom.setAttribute("role","progressbar"),this.dom.setAttribute("aria-valuemin","0"),this.dom.setAttribute("aria-valuemax",String(s)),this.dom.setAttribute("aria-valuenow",String(w)),b(this.dom,"ln-progress:change",{target:this.dom,value:e,max:s,percentage:i})}window[t]=o,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){o(document.body)}):o(document.body)})()})();
@@ -0,0 +1,150 @@
1
+ import { dispatch, guardBody } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = '[data-ln-progress]';
5
+ const DOM_ATTRIBUTE = 'lnProgress';
6
+
7
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
8
+
9
+ function constructor(domRoot) {
10
+ findElements(domRoot);
11
+ }
12
+
13
+ // Local findElements — intentional divergence from ln-core helper:
14
+ // inlines selector + constructor (no ComponentClass parameter) and
15
+ // includes a domRoot self-match branch so the childList re-init
16
+ // path picks up the inserted root itself.
17
+ function findElements(domRoot) {
18
+ const items = Array.from(domRoot.querySelectorAll(DOM_SELECTOR));
19
+
20
+ for (const item of items) {
21
+ if (!item[DOM_ATTRIBUTE]) {
22
+ item[DOM_ATTRIBUTE] = new _constructor(item);
23
+ }
24
+ }
25
+
26
+ if (domRoot.hasAttribute && domRoot.hasAttribute('data-ln-progress') && !domRoot[DOM_ATTRIBUTE]) {
27
+ domRoot[DOM_ATTRIBUTE] = new _constructor(domRoot);
28
+ }
29
+ }
30
+
31
+ function _constructor(dom) {
32
+ this.dom = dom;
33
+ this._attrObserver = null;
34
+ this._parentObserver = null;
35
+ _render.call(this);
36
+ _listenValues.call(this);
37
+ _listenParent.call(this);
38
+ return this;
39
+ }
40
+
41
+ _constructor.prototype.destroy = function () {
42
+ if (!this.dom[DOM_ATTRIBUTE]) return;
43
+ if (this._attrObserver) {
44
+ this._attrObserver.disconnect();
45
+ }
46
+ if (this._parentObserver) {
47
+ this._parentObserver.disconnect();
48
+ }
49
+ delete this.dom[DOM_ATTRIBUTE];
50
+ };
51
+
52
+ function _domObserver() {
53
+ guardBody(function () {
54
+ const observer = new MutationObserver(function (mutations) {
55
+ for (const mutation of mutations) {
56
+ if (mutation.type === "childList") {
57
+ for (const item of mutation.addedNodes) {
58
+ if (item.nodeType === 1) {
59
+ findElements(item);
60
+ }
61
+ }
62
+ } else if (mutation.type === 'attributes') {
63
+ findElements(mutation.target);
64
+ }
65
+ }
66
+ });
67
+
68
+ observer.observe(document.body, {
69
+ childList: true,
70
+ subtree: true,
71
+ attributes: true,
72
+ attributeFilter: ['data-ln-progress']
73
+ });
74
+ }, 'ln-progress');
75
+ }
76
+
77
+ _domObserver();
78
+
79
+ function _listenValues() {
80
+ const self = this;
81
+ const observer = new MutationObserver(function (mutations) {
82
+ for (const mutation of mutations) {
83
+ if (mutation.attributeName === 'data-ln-progress' || mutation.attributeName === 'data-ln-progress-max') {
84
+ _render.call(self);
85
+ }
86
+ }
87
+ });
88
+
89
+ observer.observe(this.dom, {
90
+ attributes: true,
91
+ attributeFilter: ['data-ln-progress', 'data-ln-progress-max']
92
+ });
93
+
94
+ this._attrObserver = observer;
95
+ }
96
+
97
+ function _listenParent() {
98
+ const self = this;
99
+ const parent = this.dom.parentElement;
100
+ if (!parent || !parent.hasAttribute('data-ln-progress-max')) return;
101
+
102
+ const observer = new MutationObserver(function (mutations) {
103
+ for (const mutation of mutations) {
104
+ if (mutation.attributeName === 'data-ln-progress-max') {
105
+ _render.call(self);
106
+ }
107
+ }
108
+ });
109
+
110
+ observer.observe(parent, {
111
+ attributes: true,
112
+ attributeFilter: ['data-ln-progress-max']
113
+ });
114
+
115
+ this._parentObserver = observer;
116
+ }
117
+
118
+ function _render() {
119
+ const value = parseFloat(this.dom.getAttribute('data-ln-progress')) || 0;
120
+ const parent = this.dom.parentElement;
121
+ const parentMax = parent && parent.hasAttribute('data-ln-progress-max')
122
+ ? parseFloat(parent.getAttribute('data-ln-progress-max'))
123
+ : null;
124
+ const max = parentMax || parseFloat(this.dom.getAttribute('data-ln-progress-max')) || 100;
125
+ let percentage = (max > 0) ? (value / max) * 100 : 0;
126
+
127
+ if (percentage < 0) percentage = 0;
128
+ if (percentage > 100) percentage = 100;
129
+
130
+ this.dom.style.width = percentage + '%';
131
+
132
+ const clampedValue = Math.max(0, Math.min(value, max));
133
+ this.dom.setAttribute('role', 'progressbar');
134
+ this.dom.setAttribute('aria-valuemin', '0');
135
+ this.dom.setAttribute('aria-valuemax', String(max));
136
+ this.dom.setAttribute('aria-valuenow', String(clampedValue));
137
+
138
+ dispatch(this.dom, 'ln-progress:change', { target: this.dom, value: value, max: max, percentage: percentage });
139
+ }
140
+
141
+ window[DOM_ATTRIBUTE] = constructor;
142
+
143
+ if (document.readyState === 'loading') {
144
+ document.addEventListener('DOMContentLoaded', function () {
145
+ constructor(document.body);
146
+ });
147
+ } else {
148
+ constructor(document.body);
149
+ }
150
+ })();
@@ -0,0 +1,83 @@
1
+ # ln-search
2
+
3
+ A zero-dependency, event-driven **Debounced Search Primitive** that intercepts text input, debounces keystrokes, and coordinates list-filtering states.
4
+
5
+ It acts as an intent announcer. It does not own data filtering directly; instead, it announces a normalized search query on the target element via a cancelable event, allowing tables, lists, or custom server integrations to intercept and handle the query or fall back to a built-in DOM-walking filter.
6
+
7
+ ---
8
+
9
+ ## 🧭 Philosophy & Architecture
10
+
11
+ 1. **Decoupled Search Intent:** The component does not manipulate collections directly. It debounces keystrokes (150ms trailing-edge) and dispatches a cancelable `ln-search:change` custom event on the **target** element (not the input).
12
+ 2. **The Default DOM-Walk Fallback:** If no listener calls `e.preventDefault()`, the component automatically runs a fast DOM-walk on the target's children, applying `data-ln-search-hide="true"` to non-matching elements.
13
+ 3. **Reactive Form-Restore Hook:** During page reloads or back-button navigation, browsers auto-fill input fields before scripts initialize. `ln-search` detects this pre-filled state at construction and schedules an initial search dispatch via `queueMicrotask` to ensure all listener components have fully initialized first.
14
+
15
+ ---
16
+
17
+ ## 📦 Minimal Blueprint
18
+
19
+ ### Standard List Filtering (Zero-JS Fallback)
20
+ Bind the search input to a list `id` via `data-ln-search`.
21
+ ```html
22
+ <input type="search" placeholder="Search..." data-ln-search="countries-list">
23
+
24
+ <ul id="countries-list">
25
+ <li>Argentina</li>
26
+ <li>Brazil</li>
27
+ <li>Canada</li>
28
+ </ul>
29
+ ```
30
+
31
+ ### Premium Icon Wrapper with Clear Button
32
+ When placed on a wrapper container, the component automatically resolves the nested input.
33
+ ```html
34
+ <label data-ln-search="countries-list">
35
+ <svg class="ln-icon ln-icon--sm" aria-hidden="true"><use href="#ln-search"></use></svg>
36
+ <input type="search" placeholder="Search countries...">
37
+ <button type="button" data-ln-search-clear aria-label="Clear search">
38
+ <svg class="ln-icon ln-icon--sm" aria-hidden="true"><use href="#ln-x"></use></svg>
39
+ </button>
40
+ </label>
41
+ ```
42
+
43
+ ---
44
+
45
+ ## 🛠️ Declarative API Contract
46
+
47
+ ### HTML Attributes
48
+
49
+ | Attribute | Elements | Description |
50
+ | :--- | :--- | :--- |
51
+ | `data-ln-search` | `<input>`, `<label>` | Component root and namespace. Value is the `id` of the target to filter. |
52
+ | `data-ln-search-items` | Same as root | Opt-in. Deep CSS selector (e.g. `tbody tr`) to query deep matching elements instead of direct children. |
53
+ | `data-ln-search-clear` | `<button>` | Identifies the clear button. Click clears input and triggers synchronous search reset. |
54
+ | `data-ln-search-hide` | Children of target | *State*. Automatically toggled on non-matching elements (`display: none !important`). |
55
+
56
+ ### JavaScript API (`el.lnSearch`)
57
+
58
+ | Member | Type | Description |
59
+ | :--- | :--- | :--- |
60
+ | `targetId` | `string` | The ID of the target element. |
61
+ | `input` | `HTMLInputElement` | The resolved input element driving the search. |
62
+ | `destroy()` | `() => void` | Removes listeners, cancels debounces, and tears down the instance. |
63
+
64
+ ---
65
+
66
+ ## ⚡ DOM Events
67
+
68
+ ### `ln-search:change`
69
+ Dispatched on the **target** element whenever the search term changes.
70
+ - **Cancelable**: Yes. Calling `e.preventDefault()` disables the default DOM-walk.
71
+ - **Payload (`detail`)**: `{ term: string, targetId: string }` (where `term` is lowercased and trimmed).
72
+
73
+ ---
74
+
75
+ ## ⚠️ Common Pitfalls
76
+
77
+ - **Confusing with `ln-data-table` Search:** `ln-data-table` manages its own internal search input via `data-ln-data-table-search` to join sorting and pagination into unified API request arrays. Do not place `ln-search` in front of `ln-data-table`.
78
+ - **Bypassing Debounce via Native Input Listeners:** Listening to native `input` events directly will bypass the 150ms debounce and execute expensive logic on every single keystroke. Always listen to `ln-search:change`.
79
+ - **Programmatic Value Mutations:** Assigning `input.value = "text"` programmatically does not trigger search. You must manually dispatch an `input` event:
80
+ ```javascript
81
+ input.value = 'Argentina';
82
+ input.dispatchEvent(new Event('input', { bubbles: true }));
83
+ ```