@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,442 +1,413 @@
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.
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. Around 85 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) |
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
+ `ln-progress: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 managed by `registerComponent` 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` |
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`.
234
+
235
+ ### Upload progress — driving from XHR
236
+
237
+ ```html
238
+ <form id="upload-form">
239
+ <input type="file" name="file">
240
+ <div id="upload-track" class="progress">
241
+ <div id="upload-bar" data-ln-progress="0" class="success"
242
+ aria-label="Upload progress"></div>
243
+ </div>
244
+ <button type="submit">Upload</button>
245
+ </form>
246
+ ```
247
+
248
+ ```js
249
+ document.getElementById('upload-form').addEventListener('submit', function (e) {
250
+ e.preventDefault();
251
+ const bar = document.getElementById('upload-bar');
252
+ const file = e.target.querySelector('input[type=file]').files[0];
253
+ if (!file) return;
254
+
255
+ const xhr = new XMLHttpRequest();
256
+ const fd = new FormData(e.target);
257
+
258
+ xhr.upload.addEventListener('progress', function (ev) {
259
+ if (!ev.lengthComputable) return;
260
+ const pct = Math.round((ev.loaded / ev.total) * 100);
261
+ bar.setAttribute('data-ln-progress', pct);
262
+ });
263
+
264
+ xhr.upload.addEventListener('load', function () {
265
+ bar.setAttribute('data-ln-progress', 100);
266
+ });
267
+
268
+ xhr.open('POST', '/upload');
269
+ xhr.send(fd);
270
+ });
271
+ ```
272
+
273
+ ARIA on the bar is managed automatically by `ln-progress` —
274
+ `aria-valuenow` updates on every attribute write. The `progress`
275
+ event fires often during a large upload — every 16ms or so on a fast
276
+ connection. Each call rewrites the attribute, the MutationObserver
277
+ fires, `_render` runs, and the `:change` event dispatches. If
278
+ profiling shows it as a hot spot, throttle the JS-side write to
279
+ every 100ms before calling `setAttribute`.
280
+
281
+ ### Threshold-based colour — consumer code
282
+
283
+ The component has no automatic threshold colours. `class="success"`
284
+ / `.warning` / `.error` are static — JS does not flip them based on
285
+ value. If you want red below 30% and green above 70%, that is
286
+ consumer code:
287
+
288
+ ```js
289
+ function setProgressWithThreshold(bar, value) {
290
+ bar.setAttribute('data-ln-progress', value);
291
+ bar.classList.remove('success', 'warning', 'error');
292
+ if (value < 30) bar.classList.add('error');
293
+ else if (value < 70) bar.classList.add('warning');
294
+ else bar.classList.add('success');
295
+ }
296
+ ```
297
+
298
+ The colour-class swap is a `classList` write — no MutationObserver
299
+ fires for it (the component watches `data-ln-progress` and
300
+ `data-ln-progress-max`, not `class`). The new colour applies via plain
301
+ CSS rules in `scss/components/_progress.scss`.
302
+
303
+ ## What `ln-progress` does NOT do
304
+
305
+ - **Does NOT clamp the input attribute.** It clamps the *computed
306
+ percentage* between 0 and 100 before writing the width. The
307
+ attribute can hold any number; reading
308
+ `el.getAttribute('data-ln-progress')` after writing `'150'` returns
309
+ `'150'`, not `'100'`.
310
+ - **Does NOT have an indeterminate / striped / pulsing mode.**
311
+ No animated-stripes overlay, no shimmer, no pulse. For an
312
+ indeterminate indicator on an unbounded operation, use
313
+ `@mixin loader` (`scss/config/mixins/_loader.scss`) — different
314
+ visual idiom, separate primitive.
315
+ - **Does NOT debounce or throttle rapid-fire value changes.** Every
316
+ attribute write triggers a synchronous `_render` call. For
317
+ high-frequency flows (60+ writes/sec), the consumer is responsible
318
+ for throttling.
319
+
320
+ ## Common mistakes
321
+
322
+ ### 1. Setting `style.width` manually inside the markup
323
+
324
+ ```html
325
+ <!-- WRONG — JS overwrites style.width on construction -->
326
+ <div class="progress">
327
+ <div data-ln-progress="75" class="success" style="width: 50%"></div>
328
+ </div>
329
+ ```
330
+
331
+ The constructor's first action is `_render`, which writes
332
+ `this.dom.style.width = percentage + '%'`. Whatever inline width you
333
+ set in markup is overwritten on the first paint. If you want the bar
334
+ to render at a specific width, set the value via `data-ln-progress`,
335
+ not `style.width`.
336
+
337
+ ### 2. Mixing percentage values with absolute max
338
+
339
+ ```html
340
+ <!-- WRONG — value is "50" interpreted as a count, not 50% -->
341
+ <div class="progress">
342
+ <div data-ln-progress="50" data-ln-progress-max="500" class="success"></div>
343
+ </div>
344
+ ```
345
+
346
+ This renders 50/500 = 10% of the track, probably not what the author
347
+ meant. The component does not interpret the value as a percentage; it
348
+ treats it as a count against the max. Either write the absolute count
349
+ (`250` for "50% of 500") or drop the max and use the default of 100
350
+ with the percentage as the value.
351
+
352
+ ### 3. Multiple bars sharing a parent without declaring parent-max
353
+
354
+ ```html
355
+ <!-- WRONG — three bars at 4 / 2 / 1, default max=100 each → 7% of track filled -->
356
+ <div class="progress">
357
+ <div data-ln-progress="4" class="success"></div>
358
+ <div data-ln-progress="2" class="warning"></div>
359
+ <div data-ln-progress="1" class="error"></div>
360
+ </div>
361
+ ```
362
+
363
+ If the intent was "4 + 2 + 1 = 7 items, all visible proportionally,"
364
+ declare the parent's max:
365
+
366
+ ```html
367
+ <!-- RIGHT -->
368
+ <div class="progress" data-ln-progress-max="7">
369
+ <div data-ln-progress="4" class="success"></div>
370
+ <div data-ln-progress="2" class="warning"></div>
371
+ <div data-ln-progress="1" class="error"></div>
372
+ </div>
373
+ ```
374
+
375
+ Each bar reads parent max (`7`) as the denominator, and 4/7 + 2/7 +
376
+ 1/7 fills the track exactly.
377
+
378
+ ### 4. Forgetting `class="progress"` on the track wrapper
379
+
380
+ Without the class, the wrapper has no styling — no recessed
381
+ background, no rounded edges, no overflow clip. The bar still
382
+ renders correctly but visually escapes its track.
383
+
384
+ ## 🔧 Internals
385
+
386
+ Source: `js/ln-progress/src/ln-progress.js` (~85 lines). Passive renderer — `_render` re-reads all three attributes fresh on every call; there is no cached value or max on the instance.
387
+
388
+ ### Three observers, three concerns
389
+
390
+ 1. `registerComponent`'s shared document-level observer instantiates new bars.
391
+ 2. `_attrObserver` (per bar) watches the bar's own `data-ln-progress`/`-max`.
392
+ 3. `_parentObserver` (per bar, always registered when a parent exists) watches the parent track's `data-ln-progress-max` — this is what makes a runtime change to the shared max re-render every child bar without any bar-side wiring.
393
+
394
+ ### Construction order
395
+
396
+ `_render` runs once immediately (dispatching the first `:change` before either observer is even wired), so a document-level listener attached before the bundle loads sees an event for every bar during the initial `DOMContentLoaded` scan; the two observers are attached after.
397
+
398
+ ### Render writes
399
+
400
+ Every `_render` call writes `style.width` and all four ARIA attributes (`role`, `aria-valuemin`, `aria-valuemax`, `aria-valuenow`) together, in the same pass that dispatches `:change` — there is no separate ARIA-sync step to keep in sync.
401
+
402
+ ---
403
+
404
+ ## Related
405
+
406
+ - **`@mixin progress`** (`scss/config/mixins/_progress.scss`) — the
407
+ recipe. Defines the track height, the recessed background, the
408
+ rounded full radius, the overflow clip, and the inner-bar rules
409
+ (initial `width: 0`, `transition: width var(--transition-base)`
410
+ gated through `motion-safe`, rounded right edge on the last child).
411
+ - **`scss/components/_progress.scss`** — applies the base mixin to
412
+ `.progress` and applies the `.success` / `.warning` /
413
+ `.error` colour variants.