@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.
- package/README.md +177 -0
- package/js/COMPONENTS.md +1102 -0
- package/js/index.js +41 -0
- package/js/ln-accordion/README.md +137 -0
- package/js/ln-accordion/ln-accordion.js +1 -0
- package/js/ln-accordion/src/ln-accordion.js +41 -0
- package/js/ln-ajax/README.md +91 -0
- package/js/ln-ajax/ln-ajax.js +1 -0
- package/js/ln-ajax/src/ln-ajax.js +277 -0
- package/js/ln-api-connector/README.md +150 -0
- package/js/ln-api-connector/ln-api-connector.js +1 -0
- package/js/ln-api-connector/src/ln-api-connector.js +265 -0
- package/js/ln-autoresize/README.md +80 -0
- package/js/ln-autoresize/ln-autoresize.js +1 -0
- package/js/ln-autoresize/src/ln-autoresize.js +47 -0
- package/js/ln-autosave/README.md +92 -0
- package/js/ln-autosave/ln-autosave.js +1 -0
- package/js/ln-autosave/src/ln-autosave.js +147 -0
- package/js/ln-circular-progress/README.md +161 -0
- package/js/ln-circular-progress/ln-circular-progress.js +1 -0
- package/js/ln-circular-progress/src/ln-circular-progress.js +133 -0
- package/js/ln-confirm/README.md +86 -0
- package/js/ln-confirm/_ln-confirm.scss +13 -0
- package/js/ln-confirm/ln-confirm.js +1 -0
- package/js/ln-confirm/src/ln-confirm.js +131 -0
- package/js/ln-core/crypto.js +83 -0
- package/js/ln-core/helpers.js +411 -0
- package/js/ln-core/index.js +5 -0
- package/js/ln-core/persist.js +71 -0
- package/js/ln-core/positioning.js +207 -0
- package/js/ln-core/reactive.js +74 -0
- package/js/ln-couchdb-connector/README.md +156 -0
- package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -0
- package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +348 -0
- package/js/ln-data-coordinator/README.md +165 -0
- package/js/ln-data-coordinator/ln-data-coordinator.js +1 -0
- package/js/ln-data-coordinator/src/ln-data-coordinator.js +249 -0
- package/js/ln-data-store/README.md +94 -0
- package/js/ln-data-store/ln-data-store.js +1 -0
- package/js/ln-data-store/src/ln-data-store.js +699 -0
- package/js/ln-data-table/README.md +110 -0
- package/js/ln-data-table/ln-data-table.js +1 -0
- package/js/ln-data-table/ln-data-table.scss +10 -0
- package/js/ln-data-table/src/ln-data-table.js +1103 -0
- package/js/ln-date/README.md +151 -0
- package/js/ln-date/ln-date.js +1 -0
- package/js/ln-date/src/ln-date.js +442 -0
- package/js/ln-dropdown/README.md +117 -0
- package/js/ln-dropdown/ln-dropdown.js +1 -0
- package/js/ln-dropdown/ln-dropdown.scss +15 -0
- package/js/ln-dropdown/src/ln-dropdown.js +174 -0
- package/js/ln-external-links/README.md +341 -0
- package/js/ln-external-links/ln-external-links.js +1 -0
- package/js/ln-external-links/src/ln-external-links.js +116 -0
- package/js/ln-filter/README.md +99 -0
- package/js/ln-filter/ln-filter.js +1 -0
- package/js/ln-filter/ln-filter.scss +7 -0
- package/js/ln-filter/src/ln-filter.js +404 -0
- package/js/ln-form/README.md +101 -0
- package/js/ln-form/ln-form.js +1 -0
- package/js/ln-form/src/ln-form.js +199 -0
- package/js/ln-http/README.md +89 -0
- package/js/ln-http/ln-http.js +1 -0
- package/js/ln-http/src/ln-http.js +219 -0
- package/js/ln-icons/README.md +88 -0
- package/js/ln-icons/ln-icons.js +1 -0
- package/js/ln-icons/src/ln-icons.js +169 -0
- package/js/ln-link/README.md +303 -0
- package/js/ln-link/ln-link.js +1 -0
- package/js/ln-link/src/ln-link.js +196 -0
- package/js/ln-modal/README.md +154 -0
- package/js/ln-modal/ln-modal.js +1 -0
- package/js/ln-modal/ln-modal.scss +11 -0
- package/js/ln-modal/src/ln-modal.js +201 -0
- package/js/ln-nav/README.md +70 -0
- package/js/ln-nav/ln-nav.js +1 -0
- package/js/ln-nav/src/ln-nav.js +177 -0
- package/js/ln-number/README.md +122 -0
- package/js/ln-number/ln-number.js +1 -0
- package/js/ln-number/src/ln-number.js +302 -0
- package/js/ln-popover/README.md +127 -0
- package/js/ln-popover/ln-popover.js +1 -0
- package/js/ln-popover/src/ln-popover.js +288 -0
- package/js/ln-progress/README.md +442 -0
- package/js/ln-progress/ln-progress.js +1 -0
- package/js/ln-progress/src/ln-progress.js +150 -0
- package/js/ln-search/README.md +83 -0
- package/js/ln-search/ln-search.js +1 -0
- package/js/ln-search/ln-search.scss +7 -0
- package/js/ln-search/src/ln-search.js +114 -0
- package/js/ln-sortable/README.md +95 -0
- package/js/ln-sortable/ln-sortable.js +1 -0
- package/js/ln-sortable/src/ln-sortable.js +203 -0
- package/js/ln-table/README.md +101 -0
- package/js/ln-table/ln-table-sort.js +1 -0
- package/js/ln-table/ln-table.js +1 -0
- package/js/ln-table/ln-table.scss +11 -0
- package/js/ln-table/src/ln-table-sort.js +168 -0
- package/js/ln-table/src/ln-table.js +473 -0
- package/js/ln-tabs/README.md +137 -0
- package/js/ln-tabs/ln-tabs.js +1 -0
- package/js/ln-tabs/src/ln-tabs.js +171 -0
- package/js/ln-time/README.md +81 -0
- package/js/ln-time/ln-time.js +1 -0
- package/js/ln-time/src/ln-time.js +192 -0
- package/js/ln-toast/README.md +122 -0
- package/js/ln-toast/ln-toast.js +15 -0
- package/js/ln-toast/src/ln-toast.js +210 -0
- package/js/ln-toast/template.html +14 -0
- package/js/ln-toggle/README.md +137 -0
- package/js/ln-toggle/ln-toggle.js +1 -0
- package/js/ln-toggle/src/ln-toggle.js +139 -0
- package/js/ln-tooltip/README.md +58 -0
- package/js/ln-tooltip/ln-tooltip.js +1 -0
- package/js/ln-tooltip/ln-tooltip.scss +9 -0
- package/js/ln-tooltip/src/ln-tooltip.js +169 -0
- package/js/ln-translations/README.md +96 -0
- package/js/ln-translations/ln-translations.js +1 -0
- package/js/ln-translations/src/ln-translations.js +275 -0
- package/js/ln-upload/README.md +180 -0
- package/js/ln-upload/ln-upload.js +1 -0
- package/js/ln-upload/ln-upload.scss +20 -0
- package/js/ln-upload/src/ln-upload.js +407 -0
- package/js/ln-validate/README.md +108 -0
- package/js/ln-validate/ln-validate.js +1 -0
- package/js/ln-validate/src/ln-validate.js +160 -0
- package/package.json +55 -0
- package/scss/base/_global.scss +83 -0
- package/scss/base/_reset.scss +17 -0
- package/scss/base/_typography.scss +125 -0
- package/scss/components/_accordion.scss +34 -0
- package/scss/components/_ajax.scss +15 -0
- package/scss/components/_alert.scss +5 -0
- package/scss/components/_app-shell.scss +15 -0
- package/scss/components/_avatar.scss +6 -0
- package/scss/components/_breadcrumbs.scss +33 -0
- package/scss/components/_button.scss +20 -0
- package/scss/components/_card.scss +10 -0
- package/scss/components/_chip.scss +5 -0
- package/scss/components/_circular-progress.scss +29 -0
- package/scss/components/_confirm.scss +5 -0
- package/scss/components/_data-table.scss +83 -0
- package/scss/components/_dropdown.scss +25 -0
- package/scss/components/_empty-state.scss +22 -0
- package/scss/components/_form.scss +100 -0
- package/scss/components/_layout.scss +8 -0
- package/scss/components/_link.scss +11 -0
- package/scss/components/_ln-table.scss +60 -0
- package/scss/components/_loader.scss +6 -0
- package/scss/components/_modal.scss +20 -0
- package/scss/components/_nav.scss +9 -0
- package/scss/components/_page-header.scss +10 -0
- package/scss/components/_popover.scss +10 -0
- package/scss/components/_progress.scss +17 -0
- package/scss/components/_prose.scss +5 -0
- package/scss/components/_scrollbar.scss +32 -0
- package/scss/components/_sections.scss +12 -0
- package/scss/components/_sidebar.scss +5 -0
- package/scss/components/_stat-card.scss +5 -0
- package/scss/components/_status-badge.scss +4 -0
- package/scss/components/_stepper.scss +5 -0
- package/scss/components/_table.scss +19 -0
- package/scss/components/_tabs.scss +21 -0
- package/scss/components/_timeline.scss +14 -0
- package/scss/components/_toast.scss +41 -0
- package/scss/components/_toggle.scss +81 -0
- package/scss/components/_tooltip.scss +18 -0
- package/scss/components/_translations.scss +111 -0
- package/scss/components/_upload.scss +51 -0
- package/scss/config/_breakpoints.scss +72 -0
- package/scss/config/_density.scss +117 -0
- package/scss/config/_icons.scss +37 -0
- package/scss/config/_mixins.scss +13 -0
- package/scss/config/_theme.scss +216 -0
- package/scss/config/_tokens.scss +419 -0
- package/scss/config/mixins/_accordion.scss +52 -0
- package/scss/config/mixins/_ajax.scss +39 -0
- package/scss/config/mixins/_alert.scss +82 -0
- package/scss/config/mixins/_app-shell.scss +312 -0
- package/scss/config/mixins/_avatar.scss +109 -0
- package/scss/config/mixins/_borders.scss +36 -0
- package/scss/config/mixins/_breadcrumbs.scss +72 -0
- package/scss/config/mixins/_breakpoints.scss +62 -0
- package/scss/config/mixins/_btn.scss +179 -0
- package/scss/config/mixins/_card.scss +338 -0
- package/scss/config/mixins/_chip.scss +66 -0
- package/scss/config/mixins/_circular-progress.scss +71 -0
- package/scss/config/mixins/_collapsible.scss +24 -0
- package/scss/config/mixins/_colors.scss +46 -0
- package/scss/config/mixins/_confirm.scss +31 -0
- package/scss/config/mixins/_data-table.scss +346 -0
- package/scss/config/mixins/_display.scss +32 -0
- package/scss/config/mixins/_dropdown.scss +143 -0
- package/scss/config/mixins/_empty-state.scss +30 -0
- package/scss/config/mixins/_focus.scss +55 -0
- package/scss/config/mixins/_footer.scss +42 -0
- package/scss/config/mixins/_form.scss +601 -0
- package/scss/config/mixins/_index.scss +58 -0
- package/scss/config/mixins/_interaction.scss +15 -0
- package/scss/config/mixins/_kbd.scss +22 -0
- package/scss/config/mixins/_layout.scss +117 -0
- package/scss/config/mixins/_link.scss +55 -0
- package/scss/config/mixins/_ln-table.scss +420 -0
- package/scss/config/mixins/_loader.scss +26 -0
- package/scss/config/mixins/_modal.scss +66 -0
- package/scss/config/mixins/_motion.scss +19 -0
- package/scss/config/mixins/_nav.scss +273 -0
- package/scss/config/mixins/_page-header.scss +69 -0
- package/scss/config/mixins/_popover.scss +25 -0
- package/scss/config/mixins/_position.scss +32 -0
- package/scss/config/mixins/_progress.scss +56 -0
- package/scss/config/mixins/_prose.scss +127 -0
- package/scss/config/mixins/_shadows.scss +8 -0
- package/scss/config/mixins/_sidebar.scss +95 -0
- package/scss/config/mixins/_sizing.scss +6 -0
- package/scss/config/mixins/_spacing.scss +19 -0
- package/scss/config/mixins/_stat-card.scss +68 -0
- package/scss/config/mixins/_status-badge.scss +83 -0
- package/scss/config/mixins/_stepper.scss +78 -0
- package/scss/config/mixins/_table.scss +215 -0
- package/scss/config/mixins/_tabs.scss +64 -0
- package/scss/config/mixins/_timeline.scss +69 -0
- package/scss/config/mixins/_toast.scss +148 -0
- package/scss/config/mixins/_tooltip.scss +111 -0
- package/scss/config/mixins/_transitions.scss +10 -0
- package/scss/config/mixins/_translations.scss +124 -0
- package/scss/config/mixins/_typography.scss +57 -0
- package/scss/config/mixins/_upload.scss +168 -0
- package/scss/ln-ashlar.scss +62 -0
- package/scss/tabler-icons.txt +5039 -0
- package/scss/utilities/_animations.scss +83 -0
- 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,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
|
+
.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,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
|
+
}
|