@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,108 @@
1
+ # ln-validate
2
+
3
+ A zero-dependency, high-performance **Validity Primitive** that wraps the browser's native `ValidityState` API. It manages when to evaluate inputs, how to toggle visual error indicators, and when to dispatch validation events to form-level coordinators.
4
+
5
+ It maintains no custom rules in JavaScript; instead, it relies fully on native HTML markup constraints (`required`, `minlength`, `type="email"`, `pattern`) and standard CSS classes.
6
+
7
+ ---
8
+
9
+ ## 🧭 Philosophy & Architecture
10
+
11
+ 1. **Platform First:** Browsers compile and execute rules in native C++ instantly. `ln-validate` simply acts as a visual layer: translating native `ValidityState` properties into visible HTML elements on demand.
12
+ 2. **The Touch Gate:** To prevent annoying page-load errors, validation is completely visual-silent until a field receives its first user interaction (`input` or `change`), setting its internal state `_touched = true`.
13
+ 3. **The Custom Escape Hatch:** Validation rules the browser cannot express (such as checking if an email is taken via a server lookup, or confirming passwords match) are routed through asynchronous `set-custom` / `clear-custom` events.
14
+
15
+ ---
16
+
17
+ ## 📦 Minimal Blueprint
18
+
19
+ ```html
20
+ <div class="form-element">
21
+ <label for="email">Email Address</label>
22
+ <input id="email" name="email" type="email" required minlength="5" data-ln-validate>
23
+
24
+ <ul data-ln-validate-errors>
25
+ <li class="hidden" data-ln-validate-error="required">Email is required</li>
26
+ <li class="hidden" data-ln-validate-error="typeMismatch">Invalid email format</li>
27
+ <li class="hidden" data-ln-validate-error="tooShort">Must be at least 5 characters</li>
28
+ <!-- Custom unmapped error -->
29
+ <li class="hidden" data-ln-validate-error="emailTaken">This email is already in use</li>
30
+ </ul>
31
+ </div>
32
+ ```
33
+
34
+ > [!IMPORTANT]
35
+ > The surrounding wrapper **must** carry the class `.form-element` for the sibling error lookup to succeed.
36
+
37
+ ---
38
+
39
+ ## 🛠️ Declarative API Contract
40
+
41
+ ### HTML Attributes
42
+
43
+ | Attribute | Elements | Value / Mapped Rule |
44
+ | :--- | :--- | :--- |
45
+ | `data-ln-validate` | `<input>`, `<select>`, `<textarea>` | Participation marker. Presence creates the instance. |
46
+ | `data-ln-validate-errors` | `<ul>` | Identifies the sibling error list container. |
47
+ | `data-ln-validate-error="key"` | `<li>` | Maps to standard native rules or custom keys (see list below). |
48
+
49
+ ### Mapped Native Keys
50
+
51
+ | Key | Native HTML Attribute | Browser ValidityState Property |
52
+ | :--- | :--- | :--- |
53
+ | `required` | `required` | `valueMissing` |
54
+ | `typeMismatch` | `type="email"`, `type="url"`, etc. | `typeMismatch` |
55
+ | `tooShort` | `minlength="N"` | `tooShort` |
56
+ | `tooLong` | `maxlength="N"` | `tooLong` |
57
+ | `patternMismatch` | `pattern="regex"` | `patternMismatch` |
58
+ | `rangeUnderflow` | `min="N"` | `rangeUnderflow` |
59
+ | `rangeOverflow` | `max="N"` | `rangeOverflow` |
60
+
61
+ *Note: Any key not present in the native mapping table is automatically treated as a **custom error**.*
62
+
63
+ ### JS API
64
+
65
+ Access the validation instance directly via the `lnValidate` property on the input element:
66
+
67
+ ```javascript
68
+ const input = document.getElementById('email');
69
+
70
+ // 1. Force-validate the field (returns boolean; ignores the touched gate)
71
+ const isValid = input.lnValidate.validate();
72
+
73
+ // 2. Revert the touched state, clear visual classes, and hide error lists
74
+ input.lnValidate.reset();
75
+
76
+ // 3. Live boolean check (native validity + custom errors)
77
+ if (input.lnValidate.isValid) { ... }
78
+
79
+ // 4. Tear down listeners and delete instance reference
80
+ input.lnValidate.destroy();
81
+ ```
82
+
83
+ ---
84
+
85
+ ## ⚡ DOM Events
86
+
87
+ ### Emitted
88
+
89
+ | Event | Bubbles | Payload | Description |
90
+ | :--- | :--- | :--- | :--- |
91
+ | `ln-validate:valid` | Yes | `{ target, field }` | Dispatched after every validation pass that succeeds. |
92
+ | `ln-validate:invalid` | Yes | `{ target, field }` | Dispatched after every validation pass that fails. |
93
+ | `ln-validate:destroyed` | Yes | `{ target }` | Dispatched when the validation instance is torn down. |
94
+
95
+ ### Received
96
+
97
+ | Event | Payload | Description |
98
+ | :--- | :--- | :--- |
99
+ | `ln-validate:set-custom` | `{ error: String }` | Injects a custom error key, highlights the field as invalid. |
100
+ | `ln-validate:clear-custom` | `{ error: String }` / `{}` | Clears a specific custom error, or all custom errors at once. |
101
+
102
+ ---
103
+
104
+ ## ⚠️ Common Pitfalls
105
+
106
+ - **Skipping the `.form-element` wrapper:** `ln-validate` locates sibling error elements relative to the closest `.form-element` ancestor. If you omit the wrapper class, no error lists will be toggled.
107
+ - **Relying on Native browser CSS (`:invalid`):** The browser applies `:invalid` to empty required fields on page load. **Always** use our custom classes `.ln-validate-valid` and `.ln-validate-invalid` for visual borders and focus states.
108
+ - **Injecting custom errors without validating:** Dispatching `ln-validate:set-custom` updates visual classes, but it **does not** bubble an `:invalid` event. If you need form-level coordinators (`ln-form`) to react immediately, call `input.lnValidate.validate()` right after injecting.
@@ -0,0 +1 @@
1
+ (function(){"use strict";function L(n,e,f){n.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:f||{}}))}function _(n,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){_(n,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}n()}function y(n,e,f,h){if(n.nodeType!==1)return;const v=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",p=Array.from(n.querySelectorAll(v));n.matches&&n.matches(v)&&p.push(n);for(const o of p)o[f]||(o[f]=new h(o))}function w(n,e,f,h,m={}){const v=m.extraAttributes||[],p=m.onAttributeChange||null,o=m.onInit||null;function t(i){const d=i||document.body;y(d,n,e,f),o&&o(d)}return _(function(){const i=new MutationObserver(function(l){for(let u=0;u<l.length;u++){const s=l[u];if(s.type==="childList")for(let a=0;a<s.addedNodes.length;a++){const r=s.addedNodes[a];r.nodeType===1&&(y(r,n,e,f),o&&o(r))}else s.type==="attributes"&&(p&&s.target[e]?p(s.target,s.attributeName):(y(s.target,n,e,f),o&&o(s.target)))}});let d=[];if(n.indexOf("[")!==-1){const l=/\[([\w-]+)/g;let u;for(;(u=l.exec(n))!==null;)d.push(u[1])}else d.push(n);i.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:d.concat(v)})},h),window[e]=t,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){t(document.body)}):t(document.body),t}const E={};function S(n,e){E[n]=e}function b(n){return E[n]||{ingress:e=>e,egress:e=>e}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=S,window.lnCore.getDataMapper=b),(function(){const n="data-ln-validate",e="lnValidate",f="data-ln-validate-errors",h="data-ln-validate-error",m="ln-validate-valid",v="ln-validate-invalid",p={required:"valueMissing",typeMismatch:"typeMismatch",tooShort:"tooShort",tooLong:"tooLong",patternMismatch:"patternMismatch",rangeUnderflow:"rangeUnderflow",rangeOverflow:"rangeOverflow"};if(window[e]!==void 0)return;function o(t){this.dom=t,this._touched=!1,this._customErrors=new Set;const i=this,d=t.tagName,l=t.type,u=d==="SELECT"||l==="checkbox"||l==="radio";return this._onInput=function(){i._touched=!0,i.validate()},this._onChange=function(){i._touched=!0,i.validate()},this._onSetCustom=function(s){const a=s.detail&&s.detail.error;if(!a)return;i._customErrors.add(a),i._touched=!0;const r=t.closest(".form-element");if(r){const c=r.querySelector("["+h+'="'+a+'"]');c&&c.classList.remove("hidden")}t.classList.remove(m),t.classList.add(v)},this._onClearCustom=function(s){const a=s.detail&&s.detail.error,r=t.closest(".form-element");if(a){if(i._customErrors.delete(a),r){const c=r.querySelector("["+h+'="'+a+'"]');c&&c.classList.add("hidden")}}else i._customErrors.forEach(function(c){if(r){const g=r.querySelector("["+h+'="'+c+'"]');g&&g.classList.add("hidden")}}),i._customErrors.clear();i._touched&&i.validate()},u||t.addEventListener("input",this._onInput),t.addEventListener("change",this._onChange),t.addEventListener("ln-validate:set-custom",this._onSetCustom),t.addEventListener("ln-validate:clear-custom",this._onClearCustom),this}o.prototype.validate=function(){const t=this.dom,i=t.validity,l=t.checkValidity()&&this._customErrors.size===0,u=t.closest(".form-element");if(u){const a=u.querySelector("["+f+"]");if(a){const r=a.querySelectorAll("["+h+"]");for(let c=0;c<r.length;c++){const g=r[c].getAttribute(h),C=p[g];C&&(i[C]?r[c].classList.remove("hidden"):r[c].classList.add("hidden"))}}}return t.classList.toggle(m,l),t.classList.toggle(v,!l),L(t,l?"ln-validate:valid":"ln-validate:invalid",{target:t,field:t.name}),l},o.prototype.reset=function(){this._touched=!1,this._customErrors.clear(),this.dom.classList.remove(m,v);const t=this.dom.closest(".form-element");if(t){const i=t.querySelectorAll("["+h+"]");for(let d=0;d<i.length;d++)i[d].classList.add("hidden")}},Object.defineProperty(o.prototype,"isValid",{get:function(){return this.dom.checkValidity()&&this._customErrors.size===0}}),o.prototype.destroy=function(){this.dom[e]&&(this.dom.removeEventListener("input",this._onInput),this.dom.removeEventListener("change",this._onChange),this.dom.removeEventListener("ln-validate:set-custom",this._onSetCustom),this.dom.removeEventListener("ln-validate:clear-custom",this._onClearCustom),this.dom.classList.remove(m,v),L(this.dom,"ln-validate:destroyed",{target:this.dom}),delete this.dom[e])},w(n,e,o,"ln-validate")})()})();
@@ -0,0 +1,160 @@
1
+ import { dispatch, registerComponent } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-validate';
5
+ const DOM_ATTRIBUTE = 'lnValidate';
6
+ const ERRORS_SELECTOR = 'data-ln-validate-errors';
7
+ const ERROR_SELECTOR = 'data-ln-validate-error';
8
+ const CSS_VALID = 'ln-validate-valid';
9
+ const CSS_INVALID = 'ln-validate-invalid';
10
+
11
+ const ERROR_MAP = {
12
+ required: 'valueMissing',
13
+ typeMismatch: 'typeMismatch',
14
+ tooShort: 'tooShort',
15
+ tooLong: 'tooLong',
16
+ patternMismatch: 'patternMismatch',
17
+ rangeUnderflow: 'rangeUnderflow',
18
+ rangeOverflow: 'rangeOverflow'
19
+ };
20
+
21
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
22
+
23
+ // ─── Component ─────────────────────────────────────────────
24
+
25
+ function _component(dom) {
26
+ this.dom = dom;
27
+ this._touched = false;
28
+ this._customErrors = new Set();
29
+
30
+ const self = this;
31
+ const tag = dom.tagName;
32
+ const type = dom.type;
33
+ const isChangeBased = tag === 'SELECT' || type === 'checkbox' || type === 'radio';
34
+
35
+ this._onInput = function () {
36
+ self._touched = true;
37
+ self.validate();
38
+ };
39
+
40
+ this._onChange = function () {
41
+ self._touched = true;
42
+ self.validate();
43
+ };
44
+
45
+ this._onSetCustom = function (e) {
46
+ const error = e.detail && e.detail.error;
47
+ if (!error) return;
48
+ self._customErrors.add(error);
49
+ self._touched = true;
50
+ const parent = dom.closest('.form-element');
51
+ if (parent) {
52
+ const el = parent.querySelector('[' + ERROR_SELECTOR + '="' + error + '"]');
53
+ if (el) el.classList.remove('hidden');
54
+ }
55
+ dom.classList.remove(CSS_VALID);
56
+ dom.classList.add(CSS_INVALID);
57
+ };
58
+
59
+ this._onClearCustom = function (e) {
60
+ const error = e.detail && e.detail.error;
61
+ const parent = dom.closest('.form-element');
62
+ if (error) {
63
+ self._customErrors.delete(error);
64
+ if (parent) {
65
+ const el = parent.querySelector('[' + ERROR_SELECTOR + '="' + error + '"]');
66
+ if (el) el.classList.add('hidden');
67
+ }
68
+ } else {
69
+ // clear all custom errors
70
+ self._customErrors.forEach(function (err) {
71
+ if (parent) {
72
+ const el = parent.querySelector('[' + ERROR_SELECTOR + '="' + err + '"]');
73
+ if (el) el.classList.add('hidden');
74
+ }
75
+ });
76
+ self._customErrors.clear();
77
+ }
78
+ // Re-run native validation to update visual state
79
+ if (self._touched) self.validate();
80
+ };
81
+
82
+ if (!isChangeBased) {
83
+ dom.addEventListener('input', this._onInput);
84
+ }
85
+ dom.addEventListener('change', this._onChange);
86
+ dom.addEventListener('ln-validate:set-custom', this._onSetCustom);
87
+ dom.addEventListener('ln-validate:clear-custom', this._onClearCustom);
88
+
89
+ return this;
90
+ }
91
+
92
+ _component.prototype.validate = function () {
93
+ const dom = this.dom;
94
+ const validity = dom.validity;
95
+ const nativeValid = dom.checkValidity();
96
+ const isValid = nativeValid && this._customErrors.size === 0;
97
+
98
+ // Show/hide error messages
99
+ const parent = dom.closest('.form-element');
100
+ if (parent) {
101
+ const errorList = parent.querySelector('[' + ERRORS_SELECTOR + ']');
102
+ if (errorList) {
103
+ const items = errorList.querySelectorAll('[' + ERROR_SELECTOR + ']');
104
+ for (let i = 0; i < items.length; i++) {
105
+ const errorKey = items[i].getAttribute(ERROR_SELECTOR);
106
+ const validityProp = ERROR_MAP[errorKey];
107
+ if (!validityProp) continue; // custom error — managed by set-custom/clear-custom
108
+ if (validity[validityProp]) {
109
+ items[i].classList.remove('hidden');
110
+ } else {
111
+ items[i].classList.add('hidden');
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ // Toggle CSS classes on input
118
+ dom.classList.toggle(CSS_VALID, isValid);
119
+ dom.classList.toggle(CSS_INVALID, !isValid);
120
+
121
+ // Emit event
122
+ const eventName = isValid ? 'ln-validate:valid' : 'ln-validate:invalid';
123
+ dispatch(dom, eventName, { target: dom, field: dom.name });
124
+
125
+ return isValid;
126
+ };
127
+
128
+ _component.prototype.reset = function () {
129
+ this._touched = false;
130
+ this._customErrors.clear();
131
+ this.dom.classList.remove(CSS_VALID, CSS_INVALID);
132
+
133
+ const parent = this.dom.closest('.form-element');
134
+ if (parent) {
135
+ const items = parent.querySelectorAll('[' + ERROR_SELECTOR + ']');
136
+ for (let i = 0; i < items.length; i++) {
137
+ items[i].classList.add('hidden');
138
+ }
139
+ }
140
+ };
141
+
142
+ Object.defineProperty(_component.prototype, 'isValid', {
143
+ get: function () { return this.dom.checkValidity() && this._customErrors.size === 0; }
144
+ });
145
+
146
+ _component.prototype.destroy = function () {
147
+ if (!this.dom[DOM_ATTRIBUTE]) return;
148
+ this.dom.removeEventListener('input', this._onInput);
149
+ this.dom.removeEventListener('change', this._onChange);
150
+ this.dom.removeEventListener('ln-validate:set-custom', this._onSetCustom);
151
+ this.dom.removeEventListener('ln-validate:clear-custom', this._onClearCustom);
152
+ this.dom.classList.remove(CSS_VALID, CSS_INVALID);
153
+ dispatch(this.dom, 'ln-validate:destroyed', { target: this.dom });
154
+ delete this.dom[DOM_ATTRIBUTE];
155
+ };
156
+
157
+ // ─── Init ──────────────────────────────────────────────────
158
+
159
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-validate');
160
+ })();
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@livenetworks/ashlar",
3
+ "version": "1.3.2",
4
+ "description": "LiveNetworks unified frontend library - CSS framework and vanilla JS components",
5
+ "license": "MIT",
6
+ "author": "Live Networks",
7
+ "type": "module",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/livenetworks/ln-ashlar.git"
14
+ },
15
+ "homepage": "https://github.com/livenetworks/ln-ashlar#readme",
16
+ "bugs": "https://github.com/livenetworks/ln-ashlar/issues",
17
+ "keywords": [
18
+ "css",
19
+ "scss",
20
+ "design-system",
21
+ "frontend",
22
+ "components",
23
+ "vanilla-js"
24
+ ],
25
+ "files": [
26
+ "scss/",
27
+ "js/"
28
+ ],
29
+ "exports": {
30
+ "./scss/*": "./scss/*",
31
+ "./js/*": "./js/*"
32
+ },
33
+ "scripts": {
34
+ "dev": "node scripts/build.mjs --watch",
35
+ "build": "node scripts/build.mjs && npm run build:demos",
36
+ "build:demos": "npm run build:demo-index && npm run build:demo-admin && npm run build:demo-docuflow",
37
+ "build:demo-index": "sass demo/index.scss demo/index.css --no-source-map --style=compressed",
38
+ "build:demo-admin": "sass demo/admin/src/admin.scss demo/admin/dist/admin.css --no-source-map --style=compressed && cp demo/admin/src/demo.js demo/admin/dist/demo.js && node demo/admin/src/build-pages.mjs",
39
+ "build:demo-docuflow": "sass demo/docuflow/docuflow.scss demo/docuflow/dist/docuflow.css --no-source-map --style=compressed"
40
+ },
41
+ "devDependencies": {
42
+ "@tabler/icons": "^3.41.1",
43
+ "flag-icons": "^7.5.0",
44
+ "sass-embedded": "^1.83.0",
45
+ "vite": "^6.0.0"
46
+ },
47
+ "peerDependencies": {
48
+ "tom-select": "^2.0.0"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "tom-select": {
52
+ "optional": true
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,83 @@
1
+ @use '../config/mixins' as *;
2
+
3
+ html,
4
+ body {
5
+ @include w-full;
6
+ @include h-full;
7
+ overflow: hidden;
8
+ background-color: var(--bg-recessed);
9
+ @include font-sans;
10
+ -webkit-tap-highlight-color: transparent;
11
+ -webkit-text-size-adjust: 100%;
12
+ text-size-adjust: 100%;
13
+ line-height: 1.5;
14
+ }
15
+
16
+ body {
17
+ font-size: var(--text-body-md);
18
+ line-height: var(--lh-body-md);
19
+ color: var(--color-fg);
20
+ -webkit-font-smoothing: antialiased;
21
+ -moz-osx-font-smoothing: grayscale;
22
+ }
23
+
24
+ a {
25
+ // Derive hover from --color-accent at this scope. Same hue/saturation,
26
+ // lightness reduced by 8 (number form — see _btn.scss for the syntax
27
+ // rationale). Local derivation means a semantic-color parent (.error,
28
+ // .success, etc.) that rebinds --color-primary cascades to the hover
29
+ // because var(--color-accent) re-resolves on the anchor element.
30
+ --color-accent-hover: hsl(from var(--color-accent) h s calc(l - 8));
31
+
32
+ color: var(--color-accent);
33
+ text-decoration: none;
34
+ @include transition-colors;
35
+ @include relative;
36
+
37
+ &:hover {
38
+ color: var(--color-accent-hover);
39
+ }
40
+ }
41
+
42
+ // ─── All buttons — structure + neutral colors ───────────────────────────────
43
+ // Every <button> gets padding, font, and a neutral hover out of the box.
44
+ // Icon/close buttons re-bind --padding-y/--padding-x on the parent scope's
45
+ // descendant button selector to tighten the tap area.
46
+
47
+ button,
48
+ input[type="submit"],
49
+ input[type="reset"],
50
+ input[type="button"] {
51
+ --padding-x: var(--btn-padding-x);
52
+ --padding-y: var(--btn-padding-y);
53
+ --gap: var(--size-xs-up);
54
+ }
55
+
56
+
57
+ button,
58
+ input[type="submit"],
59
+ input[type="reset"],
60
+ input[type="button"] {
61
+ @include button-base;
62
+ }
63
+
64
+ // ─── Submit — accent variant via @mixin btn ─────────────────────────────────
65
+ // Color-only delta on top of button-base. Override --color-primary on
66
+ // the element or parent (e.g. .delete-action { --color-primary: var(--color-error); })
67
+ // for color variants.
68
+
69
+ button[type="submit"],
70
+ input[type="submit"] {
71
+ @include btn;
72
+ }
73
+
74
+ img {
75
+ max-width: 100%;
76
+ height: auto;
77
+ display: block;
78
+ }
79
+
80
+ ::selection {
81
+ background-color: hsl(var(--color-primary) / 0.2);
82
+ color: var(--color-fg);
83
+ }
@@ -0,0 +1,17 @@
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ [hidden] {
8
+ display: none !important;
9
+ }
10
+
11
+ ul, ol, ul li, ol li {
12
+ list-style: none;
13
+ }
14
+
15
+ html {
16
+ font-size: 16px;
17
+ }
@@ -0,0 +1,125 @@
1
+ @use '../config/mixins' as *;
2
+
3
+ h1,
4
+ h2,
5
+ h3,
6
+ h4,
7
+ h5,
8
+ h6 {
9
+ color: var(--color-fg);
10
+ @include font-bold;
11
+ }
12
+
13
+ // The global size-md bottom margin is the default for h1 rendered inline
14
+ // with surrounding content (articles, page body, demo index). Containers
15
+ // that provide their own spacing via `gap` — notably `@include page-header`
16
+ // — intentionally zero this margin on their internal h1 so the gap is the
17
+ // sole source of vertical rhythm. See scss/config/mixins/_page-header.scss.
18
+ h1 {
19
+ @include typography(display-sm);
20
+ @include tracking-tight;
21
+ margin-bottom: var(--size-md);
22
+ }
23
+
24
+ h2 {
25
+ @include typography(heading-md);
26
+ @include tracking-tight;
27
+ margin-bottom: var(--size-md);
28
+ }
29
+
30
+ h3 {
31
+ @include typography(heading-sm);
32
+ @include font-semibold;
33
+ margin-bottom: var(--size-sm-up);
34
+ }
35
+
36
+ h4 {
37
+ @include typography(title-md);
38
+ @include font-semibold;
39
+ margin-bottom: var(--size-sm);
40
+ }
41
+
42
+ h5 {
43
+ @include typography(title-sm);
44
+ @include font-semibold;
45
+ margin-bottom: var(--size-sm);
46
+ }
47
+
48
+ h6 {
49
+ @include typography(label-md);
50
+ @include font-semibold;
51
+ margin-bottom: var(--size-xs);
52
+ }
53
+
54
+ p {
55
+ font-size: var(--text-body-md);
56
+ color: var(--color-fg);
57
+ line-height: 1.75;
58
+ margin-bottom: var(--size-md);
59
+
60
+ &:last-child {
61
+ margin-bottom: 0;
62
+ }
63
+ }
64
+
65
+ strong,
66
+ b {
67
+ @include font-semibold;
68
+ }
69
+
70
+ em,
71
+ i {
72
+ font-style: italic;
73
+ }
74
+
75
+ small {
76
+ font-size: var(--text-sm);
77
+ --color-fg: var(--fg-muted);
78
+ color: var(--color-fg);
79
+ }
80
+
81
+ code {
82
+ @include font-mono;
83
+ @include text-sm;
84
+ --color-bg: var(--bg-recessed);
85
+ background-color: var(--color-bg);
86
+ padding: var(--size-2xs) var(--size-xs);
87
+ @include rounded-sm;
88
+ }
89
+
90
+ pre {
91
+ @include font-mono;
92
+ @include text-sm;
93
+ --color-bg: var(--bg-recessed);
94
+ background-color: var(--color-bg);
95
+ padding: var(--size-md);
96
+ @include rounded-md;
97
+ @include border;
98
+ @include overflow-x-auto;
99
+ margin-bottom: var(--size-md);
100
+
101
+ code {
102
+ background: none;
103
+ padding: 0;
104
+ }
105
+ }
106
+
107
+ table {
108
+ margin-bottom: var(--size-md);
109
+ }
110
+
111
+ kbd {
112
+ @include kbd;
113
+ }
114
+
115
+ blockquote {
116
+ border-left: 3px solid var(--color-accent);
117
+ --color-bg: var(--bg-sunken);
118
+ background-color: var(--color-bg);
119
+ padding: var(--size-md);
120
+ @include rounded-sm;
121
+ margin-bottom: var(--size-md);
122
+ --color-fg: var(--fg-muted);
123
+ color: var(--color-fg);
124
+ font-style: italic;
125
+ }
@@ -0,0 +1,34 @@
1
+ @use '../config/mixins' as *;
2
+
3
+ // ==========================================================================
4
+ // Accordion — styled list with chevron rotation
5
+ // ==========================================================================
6
+ // [data-ln-accordion] gets the full styled accordion: bordered card,
7
+ // dividers, trigger hover, chevron rotate on open.
8
+ //
9
+ // To style a custom selector instead of (or in addition to) the data
10
+ // attribute: #my-list { @include accordion; }
11
+ // ==========================================================================
12
+
13
+ [data-ln-accordion] {
14
+ @include accordion;
15
+
16
+ > li {
17
+ // Trigger — element-level colors, typography, hover
18
+ > [data-ln-toggle-for] {
19
+ @include font-medium;
20
+ color: var(--color-fg);
21
+ @include transition;
22
+
23
+ &:hover {
24
+ --color-bg: var(--bg-sunken);
25
+ background: var(--color-bg);
26
+ }
27
+
28
+ .ln-chevron {
29
+ --color-fg: var(--fg-subtle);
30
+ color: var(--color-fg);
31
+ }
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,15 @@
1
+ @use '../config/mixins' as *;
2
+
3
+ .ln-ajax--loading {
4
+ @include ajax-loading;
5
+ }
6
+
7
+ .ln-ajax-spinner {
8
+ @include ajax-spinner;
9
+ }
10
+
11
+ @keyframes ln-ajax-spin {
12
+ to {
13
+ transform: translate(-50%, -50%) rotate(360deg);
14
+ }
15
+ }
@@ -0,0 +1,5 @@
1
+ @use '../config/mixins' as *;
2
+
3
+ .alert { @include alert; }
4
+ .alert.banner { @include alert-banner; }
5
+ .alert[data-ln-toggle="close"] { display: none; }
@@ -0,0 +1,15 @@
1
+ @use '../config/mixins' as *;
2
+
3
+ .app-wrapper { @include app-wrapper; }
4
+ .app-header { @include app-header; }
5
+ .app-main { @include app-main; }
6
+ .app-sidebar { @include sidebar; @include sidebar-drawer; }
7
+ .app-scrim { @include app-scrim; }
8
+ .app-footer { @include app-footer; }
9
+ .app-content-wrapper { @include app-content-wrapper; }
10
+
11
+ // Header region structural classes — same tier as .app-header itself.
12
+ .header-left { @include app-header-left; }
13
+ .header-right { @include app-header-right; }
14
+ .header-actions { @include app-header-actions; }
15
+ .header-avatar { @include header-avatar; }
@@ -0,0 +1,6 @@
1
+ @use '../config/mixins' as *;
2
+
3
+ .avatar { @include avatar; }
4
+ .avatar-sm { @include avatar-sm; }
5
+ .avatar-lg { @include avatar-lg; }
6
+ .avatar-xl { @include avatar-xl; }
@@ -0,0 +1,33 @@
1
+ @use '../config/mixins' as *;
2
+
3
+ // Prototype-tier class — available out of the box for prototyping and
4
+ // inspector experimentation. Production projects apply the mixin on
5
+ // their own semantic selector:
6
+ //
7
+ // #my-breadcrumbs { @include breadcrumbs; }
8
+ .breadcrumbs {
9
+ @include breadcrumbs;
10
+
11
+ > ol {
12
+ > li + li::before {
13
+ --color-fg: var(--fg-subtle);
14
+ color: var(--color-fg);
15
+ }
16
+
17
+ a {
18
+ --color-fg: var(--fg-muted);
19
+ color: var(--color-fg);
20
+ text-decoration: none;
21
+ @include transition-colors;
22
+
23
+ &:hover {
24
+ color: var(--color-accent);
25
+ }
26
+ }
27
+
28
+ [aria-current="page"] {
29
+ color: var(--color-fg);
30
+ @include font-medium;
31
+ }
32
+ }
33
+ }