@livenetworks/ashlar 1.4.1 → 1.5.1

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.
@@ -1,168 +1,156 @@
1
- import { guardBody, dispatch } from '../../ln-core';
2
- import { persistGet, persistSet } from '../../ln-core';
3
-
4
- (function () {
5
- const DOM_ATTRIBUTE = 'lnTableSort';
6
- const SORT_ATTR = 'data-ln-sort';
7
- const SORT_ACTIVE_ATTR = 'data-ln-sort-active';
8
-
9
- if (window[DOM_ATTRIBUTE] !== undefined) return;
10
-
11
- // ─── Constructor ───────────────────────────────────────────
12
- // Auto-initializes any <table> that contains th[data-ln-sort] headers.
13
-
14
- function constructor(domRoot) {
15
- findElements(domRoot);
16
- }
17
-
18
- // Local findElements — intentional divergence from ln-core helper: tag selector ('table') + two-arg constructor (table, ths).
19
- function findElements(root) {
20
- const tables = Array.from(root.querySelectorAll('table'));
21
- if (root.tagName === 'TABLE') tables.push(root);
22
-
23
- tables.forEach(function (table) {
24
- if (table[DOM_ATTRIBUTE]) return;
25
- const ths = Array.from(table.querySelectorAll('th[' + SORT_ATTR + ']'));
26
- if (ths.length) table[DOM_ATTRIBUTE] = new _component(table, ths);
27
- });
28
- }
29
-
30
- // ─── Component ─────────────────────────────────────────────
31
-
32
- function _setSortIcon(th, dir) {
33
- const icons = th.querySelectorAll('[data-ln-sort-icon]');
34
- icons.forEach(function (icon) {
35
- const val = icon.getAttribute('data-ln-sort-icon');
36
- if (dir === null || dir === undefined) {
37
- // Neutral: show the no-value icon, hide asc/desc
38
- icon.classList.toggle('hidden', val !== null && val !== '');
39
- } else {
40
- // Show matching direction, hide others
41
- icon.classList.toggle('hidden', val !== dir);
42
- }
43
- });
44
- }
45
-
46
- function _component(table, ths) {
47
- this.table = table;
48
- this.ths = ths;
49
- this._col = -1;
50
- this._dir = null;
51
- const self = this;
52
-
53
- ths.forEach(function (th, index) {
54
- if (th[DOM_ATTRIBUTE + 'Bound']) return;
55
- th[DOM_ATTRIBUTE + 'Bound'] = true;
56
-
57
- th._lnSortClick = function (e) {
58
- // Don't sort when user clicks an interactive child (filter button, etc.)
59
- const interactive = e.target.closest('button, a, input, select, textarea, [data-ln-dropdown]');
60
- if (interactive && interactive !== th) return;
61
- self._handleClick(index, th);
62
- };
63
- th.addEventListener('click', th._lnSortClick);
64
- });
65
-
66
- // ─── Restore persisted sort ───────────────────────────────
67
- const persistRoot = table.closest('[data-ln-table][data-ln-persist]');
68
- if (persistRoot) {
69
- const saved = persistGet('table-sort', persistRoot);
70
- if (saved && saved.dir && saved.col >= 0 && saved.col < ths.length) {
71
- // Programmatically trigger the saved sort
72
- this._handleClick(saved.col, ths[saved.col]);
73
- if (saved.dir === 'desc') {
74
- this._handleClick(saved.col, ths[saved.col]);
75
- }
76
- }
77
- }
78
-
79
- return this;
80
- }
81
-
82
- _component.prototype._handleClick = function (colIndex, th) {
83
- let newDir;
84
-
85
- if (this._col !== colIndex) {
86
- newDir = 'asc';
87
- } else if (this._dir === 'asc') {
88
- newDir = 'desc';
89
- } else if (this._dir === 'desc') {
90
- newDir = null;
91
- } else {
92
- newDir = 'asc';
93
- }
94
-
95
- this.ths.forEach(function (t) {
96
- t.removeAttribute(SORT_ACTIVE_ATTR);
97
- _setSortIcon(t, null);
98
- });
99
-
100
- if (newDir === null) {
101
- this._col = -1;
102
- this._dir = null;
103
- } else {
104
- this._col = colIndex;
105
- this._dir = newDir;
106
- th.setAttribute(SORT_ACTIVE_ATTR, newDir);
107
- _setSortIcon(th, newDir);
108
- }
109
-
110
- dispatch(this.table, 'ln-table:sort', {
111
- column: colIndex,
112
- sortType: th.getAttribute(SORT_ATTR),
113
- direction: newDir
114
- });
115
- const persistRoot = this.table.closest('[data-ln-table][data-ln-persist]');
116
- if (persistRoot) {
117
- if (newDir === null) {
118
- persistSet('table-sort', persistRoot, null);
119
- } else {
120
- persistSet('table-sort', persistRoot, { col: colIndex, dir: newDir });
121
- }
122
- }
123
- };
124
-
125
- _component.prototype.destroy = function () {
126
- if (!this.table[DOM_ATTRIBUTE]) return;
127
- this.ths.forEach(function (th) {
128
- if (th._lnSortClick) {
129
- th.removeEventListener('click', th._lnSortClick);
130
- delete th._lnSortClick;
131
- }
132
- delete th[DOM_ATTRIBUTE + 'Bound'];
133
- });
134
- delete this.table[DOM_ATTRIBUTE];
135
- };
136
-
137
- // ─── DOM Observer ──────────────────────────────────────────
138
-
139
- function _domObserver() {
140
- guardBody(function () {
141
- const observer = new MutationObserver(function (mutations) {
142
- mutations.forEach(function (mutation) {
143
- if (mutation.type === 'childList') {
144
- mutation.addedNodes.forEach(function (node) {
145
- if (node.nodeType === 1) findElements(node);
146
- });
147
- } else if (mutation.type === 'attributes') {
148
- findElements(mutation.target);
149
- }
150
- });
151
- });
152
- observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: [SORT_ATTR] });
153
- }, 'ln-table-sort');
154
- }
155
-
156
- // ─── Init ──────────────────────────────────────────────────
157
-
158
- window[DOM_ATTRIBUTE] = constructor;
159
- _domObserver();
160
-
161
- if (document.readyState === 'loading') {
162
- document.addEventListener('DOMContentLoaded', function () {
163
- constructor(document.body);
164
- });
165
- } else {
166
- constructor(document.body);
167
- }
168
- })();
1
+ import { guardBody, dispatch } from '../../ln-core';
2
+ import { persistGet, persistSet } from '../../ln-core';
3
+
4
+ (function () {
5
+ const DOM_ATTRIBUTE = 'lnTableSort';
6
+ const SORT_ATTR = 'data-ln-sort';
7
+ const BUTTON_HOOK = 'data-ln-table-sort';
8
+
9
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
10
+
11
+ // ─── Constructor ───────────────────────────────────────────
12
+ // Auto-initializes any <table> that contains th[data-ln-sort] headers.
13
+
14
+ function constructor(domRoot) {
15
+ findElements(domRoot);
16
+ }
17
+
18
+ // Local findElements — intentional divergence from ln-core helper: tag selector ('table') + two-arg constructor (table, ths).
19
+ function findElements(root) {
20
+ const tables = Array.from(root.querySelectorAll('table'));
21
+ if (root.tagName === 'TABLE') tables.push(root);
22
+
23
+ tables.forEach(function (table) {
24
+ if (table[DOM_ATTRIBUTE]) return;
25
+ const ths = Array.from(table.querySelectorAll('th[' + SORT_ATTR + ']'));
26
+ if (ths.length) table[DOM_ATTRIBUTE] = new _component(table, ths);
27
+ });
28
+ }
29
+
30
+ // ─── Component ─────────────────────────────────────────────
31
+
32
+ function _component(table, ths) {
33
+ this.table = table;
34
+ this.ths = ths;
35
+ this._col = -1;
36
+ this._dir = null;
37
+ const self = this;
38
+
39
+ ths.forEach(function (th, index) {
40
+ if (th[DOM_ATTRIBUTE + 'Bound']) return;
41
+ th[DOM_ATTRIBUTE + 'Bound'] = true;
42
+
43
+ // D1/Option A: bind click to the sort button, not the whole <th>.
44
+ // Columns without a [data-ln-table-sort] button skip binding.
45
+ const btn = th.querySelector('[' + BUTTON_HOOK + ']');
46
+ if (!btn) return;
47
+
48
+ btn._lnSortClick = function () {
49
+ self._handleClick(index, th);
50
+ };
51
+ btn.addEventListener('click', btn._lnSortClick);
52
+ });
53
+
54
+ // ─── Restore persisted sort ───────────────────────────────
55
+ const persistRoot = table.closest('[data-ln-table][data-ln-persist]');
56
+ if (persistRoot) {
57
+ const saved = persistGet('table-sort', persistRoot);
58
+ if (saved && saved.dir && saved.col >= 0 && saved.col < ths.length) {
59
+ // Programmatically trigger the saved sort
60
+ this._handleClick(saved.col, ths[saved.col]);
61
+ if (saved.dir === 'desc') {
62
+ this._handleClick(saved.col, ths[saved.col]);
63
+ }
64
+ }
65
+ }
66
+
67
+ return this;
68
+ }
69
+
70
+ _component.prototype._handleClick = function (colIndex, th) {
71
+ let newDir;
72
+
73
+ if (this._col !== colIndex) {
74
+ newDir = 'asc';
75
+ } else if (this._dir === 'asc') {
76
+ newDir = 'desc';
77
+ } else if (this._dir === 'desc') {
78
+ newDir = null;
79
+ } else {
80
+ newDir = 'asc';
81
+ }
82
+
83
+ // JS-2: use state classes on <th>; CSS owns icon visibility.
84
+ this.ths.forEach(function (t) {
85
+ t.classList.remove('ln-sort-asc', 'ln-sort-desc');
86
+ });
87
+
88
+ if (newDir === null) {
89
+ this._col = -1;
90
+ this._dir = null;
91
+ } else {
92
+ this._col = colIndex;
93
+ this._dir = newDir;
94
+ th.classList.add(newDir === 'asc' ? 'ln-sort-asc' : 'ln-sort-desc');
95
+ }
96
+
97
+ dispatch(this.table, 'ln-table:sort', {
98
+ column: colIndex,
99
+ sortType: th.getAttribute(SORT_ATTR),
100
+ direction: newDir
101
+ });
102
+ const persistRoot = this.table.closest('[data-ln-table][data-ln-persist]');
103
+ if (persistRoot) {
104
+ if (newDir === null) {
105
+ persistSet('table-sort', persistRoot, null);
106
+ } else {
107
+ persistSet('table-sort', persistRoot, { col: colIndex, dir: newDir });
108
+ }
109
+ }
110
+ };
111
+
112
+ _component.prototype.destroy = function () {
113
+ if (!this.table[DOM_ATTRIBUTE]) return;
114
+ this.ths.forEach(function (th) {
115
+ const btn = th.querySelector('[' + BUTTON_HOOK + ']');
116
+ if (btn && btn._lnSortClick) {
117
+ btn.removeEventListener('click', btn._lnSortClick);
118
+ delete btn._lnSortClick;
119
+ }
120
+ delete th[DOM_ATTRIBUTE + 'Bound'];
121
+ });
122
+ delete this.table[DOM_ATTRIBUTE];
123
+ };
124
+
125
+ // ─── DOM Observer ──────────────────────────────────────────
126
+
127
+ function _domObserver() {
128
+ guardBody(function () {
129
+ const observer = new MutationObserver(function (mutations) {
130
+ mutations.forEach(function (mutation) {
131
+ if (mutation.type === 'childList') {
132
+ mutation.addedNodes.forEach(function (node) {
133
+ if (node.nodeType === 1) findElements(node);
134
+ });
135
+ } else if (mutation.type === 'attributes') {
136
+ findElements(mutation.target);
137
+ }
138
+ });
139
+ });
140
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: [SORT_ATTR] });
141
+ }, 'ln-table-sort');
142
+ }
143
+
144
+ // ─── Init ──────────────────────────────────────────────────
145
+
146
+ window[DOM_ATTRIBUTE] = constructor;
147
+ _domObserver();
148
+
149
+ if (document.readyState === 'loading') {
150
+ document.addEventListener('DOMContentLoaded', function () {
151
+ constructor(document.body);
152
+ });
153
+ } else {
154
+ constructor(document.body);
155
+ }
156
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livenetworks/ashlar",
3
- "version": "1.4.1",
3
+ "version": "1.5.1",
4
4
  "description": "LiveNetworks unified frontend library - CSS framework and vanilla JS components",
5
5
  "license": "MIT",
6
6
  "author": "Live Networks",
@@ -32,8 +32,9 @@
32
32
  },
33
33
  "scripts": {
34
34
  "dev": "node scripts/build.mjs --watch",
35
- "build": "node scripts/build.mjs && npm run build:demos",
35
+ "build": "node scripts/build.mjs && npm run build:dev-css && npm run build:demos",
36
36
  "build:demos": "npm run build:demo-index && npm run build:demo-admin && npm run build:demo-docuflow",
37
+ "build:dev-css": "sass scss/ln-ashlar-dev.scss demo/dist/ln-ashlar-dev.css --no-source-map --style=compressed",
37
38
  "build:demo-index": "sass demo/index.scss demo/index.css --no-source-map --style=compressed",
38
39
  "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
40
  "build:demo-docuflow": "sass demo/docuflow/docuflow.scss demo/docuflow/dist/docuflow.css --no-source-map --style=compressed"
@@ -0,0 +1,25 @@
1
+ @use '../config/mixins' as *;
2
+
3
+ // Filter dropdown content chrome (search input + options list; NO clear button —
4
+ // the data-ln-filter-reset "All" sentinel is the reset affordance).
5
+ // Floating shell is supplied separately:
6
+ // • popover-driven (ln-filter static): @mixin popover on the [data-ln-popover] wrapper
7
+ // • ln-data-table dynamic clone: floating-panel + box below
8
+ .column-filter-dropdown {
9
+ @include filter-dropdown;
10
+ }
11
+
12
+ // ln-data-table appends its cloned dropdown directly into the <th> and
13
+ // manages its own open/close — it does NOT route through ln-popover, so it
14
+ // needs the floating shell applied here (the popover path gets it from
15
+ // @mixin popover instead).
16
+ [data-ln-data-table] .column-filter-dropdown {
17
+ @include floating-panel;
18
+ position: absolute;
19
+ top: 100%;
20
+ right: 0;
21
+ left: auto;
22
+ margin-top: var(--size-xs);
23
+ min-width: 14rem;
24
+ padding: var(--size-sm);
25
+ }
@@ -2,8 +2,6 @@
2
2
 
3
3
  [data-ln-table] {
4
4
  @include ln-table;
5
- @include ln-table-column-filter;
6
- @include ln-table-sort;
7
5
  }
8
6
 
9
7
  // In-thead toolbar — first thead row's colspan th.
@@ -58,3 +56,4 @@
58
56
  .ln-table__spacer {
59
57
  @include ln-table-spacer-row;
60
58
  }
59
+
@@ -2,7 +2,6 @@
2
2
 
3
3
  table {
4
4
  @include table-base;
5
- @include table-filter-button;
6
5
 
7
6
  tr.section-header td {
8
7
  @include table-section-header;
@@ -17,3 +16,58 @@ table {
17
16
  table { @include table-responsive; }
18
17
  }
19
18
  }
19
+
20
+ // ─── Shared Column Sort & Filter Buttons ────────────────────────
21
+ // CSS targets the unprefixed structural helper class on the button, NOT the data-ln-* JS
22
+ // hook. .table-sort / .table-filter are thin bindings that mirror the mixins; a consumer
23
+ // may instead @include table-sort / table-filter on their own selector (the class is a
24
+ // convenience, not a mandate — see "Naming & class architecture"). Behavior hooks
25
+ // (data-ln-col-sort / data-ln-table-sort / data-ln-popover-for / data-ln-col-filter) are
26
+ // the per-system functional layer and intentionally differ.
27
+
28
+ .table-sort { @include table-sort; }
29
+ th:hover .table-sort { opacity: 0.7; }
30
+
31
+ // Active sort — JS-state class on the <th>, set by sort JS (client) or server.
32
+ .ln-sort-asc .table-sort,
33
+ .ln-sort-desc .table-sort { @include table-sort-active; }
34
+
35
+ .ln-sort-asc .table-sort {
36
+ [data-ln-sort-icon="none"] { display: none; }
37
+ [data-ln-sort-icon="asc"] { display: inline-block; }
38
+ }
39
+ .ln-sort-desc .table-sort {
40
+ [data-ln-sort-icon="none"] { display: none; }
41
+ [data-ln-sort-icon="desc"] { display: inline-block; }
42
+ }
43
+
44
+ .table-filter { @include table-filter; }
45
+ th:hover .table-filter { opacity: 0.7; }
46
+
47
+ // Active filter — JS-state class set DIRECTLY on the button (self-selector), combined with
48
+ // the structural helper class. Fixes the latent ln-data-table bug: JS toggles
49
+ // .ln-filter-active on the button itself, but the old CSS used a descendant combinator
50
+ // (.ln-filter-active [data-ln-col-filter]) that never matched, so the dot never rendered.
51
+ // See D2.
52
+ .table-filter.ln-filter-active { @include table-filter-active; }
53
+
54
+
55
+
56
+ // ─── Data Table JS-State Classes ───────────────────────────────
57
+ // JS-added state classes only. Row chrome (clickable cursor/hover, action
58
+ // buttons, sticky header) is a CONSUMER concern — applied via semantic
59
+ // selectors in project SCSS, never keyed off the data-ln-* JS hooks.
60
+
61
+ .ln-row-selected {
62
+ @include table-row-selected;
63
+ }
64
+
65
+ .ln-row-focused {
66
+ @include table-row-focused;
67
+ }
68
+
69
+ // Virtual-scroll spacer
70
+ .ln-data-table__spacer {
71
+ @include table-spacer-row;
72
+ }
73
+
@@ -0,0 +1,37 @@
1
+ @use 'colors' as *;
2
+ @use 'spacing' as *;
3
+ @use 'position' as *;
4
+
5
+ // ─── DOM Diagnostics & Linter Mixins ───────────────────────────
6
+ // These mixins are for developer-only diagnostic overlays and validations.
7
+ // They should be completely isolated from production bundles.
8
+
9
+ // Generic block element DOM diagnostic error
10
+ // Applies a red dashed border and a prominent warning banner at the top.
11
+ @mixin dev-dom-error($message) {
12
+ outline: 2px dashed hsl(var(--color-error));
13
+ @include relative;
14
+
15
+ &::before {
16
+ content: " ⚠ #{$message}";
17
+ display: block;
18
+ background: color-mix(in srgb, hsl(var(--color-error)) 15%, transparent);
19
+ color: hsl(var(--color-error));
20
+ padding: var(--size-xs) var(--size-sm);
21
+ font-size: var(--text-xs, 0.75rem);
22
+ font-weight: bold;
23
+ border-bottom: 1px solid hsl(var(--color-strong));
24
+ }
25
+ }
26
+
27
+ // Generic inline/element-level DOM diagnostic warning
28
+ // Appends a red inline text warning adjacent to the targeted element.
29
+ @mixin dev-inline-error($message) {
30
+ &::after {
31
+ content: " ⚠ #{$message}";
32
+ color: hsl(var(--color-error));
33
+ font-weight: normal;
34
+ white-space: nowrap;
35
+ margin-inline-start: var(--size-xs, 0.5rem);
36
+ }
37
+ }
@@ -0,0 +1,49 @@
1
+ @use 'spacing' as *;
2
+ @use 'display' as *;
3
+ @use 'interaction' as *;
4
+ @use 'transitions' as *;
5
+ @use 'form' as *;
6
+
7
+ // ─── Filter dropdown — content chrome only ─────────────────────────
8
+ // The floating shell (chrome, fixed positioning, min-width, show/hide)
9
+ // comes from `@mixin popover` on the popover-driven path, OR from
10
+ // `@mixin floating-panel` + scroll box on the ln-data-table dynamic
11
+ // path (see components/_filter.scss). This mixin styles ONLY the inner
12
+ // content: scrollable options box, search input, and the options list
13
+ // (pills via check-list).
14
+ //
15
+ // NOTE: there is intentionally NO clear-button block. The redundant
16
+ // "Clear Filter" button was removed library-wide — the `data-ln-filter-reset`
17
+ // "All" sentinel checkbox already performs the reset. Do not re-introduce a
18
+ // `[data-ln-filter-clear]` selector here.
19
+ //
20
+ // Tunables (consumer-overridable via --var with sane defaults):
21
+ // --filter-dropdown-max-height (default 20rem)
22
+ // --filter-dropdown-overflow-y (default auto)
23
+ // --filter-dropdown-text (default var(--text-xs)) ← sm density
24
+ // --filter-dropdown-item-pad-y (default var(--size-3xs))
25
+ //
26
+ // Usage:
27
+ // .column-filter-dropdown { @include filter-dropdown; }
28
+
29
+ @mixin filter-dropdown {
30
+ max-height: var(--filter-dropdown-max-height, 20rem);
31
+ overflow-y: var(--filter-dropdown-overflow-y, auto);
32
+ text-transform: none; // reset inherited header uppercase
33
+
34
+ [data-ln-filter-search] {
35
+ @include form-input;
36
+ font-size: var(--filter-dropdown-text, var(--text-xs));
37
+ --padding-y: var(--size-xs);
38
+ --padding-x: var(--size-sm);
39
+ width: 100%;
40
+ margin-bottom: var(--size-sm);
41
+ }
42
+
43
+ [data-ln-filter-options] {
44
+ @include check-list;
45
+ font-size: var(--filter-dropdown-text, var(--text-xs));
46
+
47
+ li { padding: var(--filter-dropdown-item-pad-y, var(--size-3xs)) 0; }
48
+ }
49
+ }
@@ -25,6 +25,7 @@
25
25
  @forward 'nav';
26
26
  @forward 'btn';
27
27
  @forward 'form';
28
+ @forward 'filter';
28
29
  @forward 'modal';
29
30
  @forward 'breadcrumbs';
30
31
  @forward 'loader';
@@ -53,6 +54,6 @@
53
54
  @forward 'toast';
54
55
  @forward 'upload';
55
56
  @forward 'translations';
56
- @forward 'data-table';
57
57
  @forward 'ln-table';
58
58
  @forward 'ajax';
59
+ @forward 'diagnostics';