@collectivetrait/lineaui 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/LICENSE +21 -0
- package/README.md +113 -2
- package/config.json +3 -0
- package/dist/blocks.css +5042 -0
- package/dist/blocks.css.map +1 -0
- package/dist/bundle.js +873 -0
- package/dist/core.css +1641 -0
- package/dist/core.css.map +1 -0
- package/dist/icons-manifest.json +1 -0
- package/dist/icons.css +158 -0
- package/dist/styles.css +10546 -0
- package/dist/styles.css.map +1 -0
- package/dist/theme-init.js +8 -0
- package/dist/utilities.css +3865 -0
- package/dist/utilities.css.map +1 -0
- package/humans.txt +8 -0
- package/js/config.js +3 -0
- package/js/index.js +45 -0
- package/js/modules/copy.js +64 -0
- package/js/modules/dialog.js +55 -0
- package/js/modules/dismiss.js +43 -0
- package/js/modules/dropdown.js +108 -0
- package/js/modules/gallery.js +84 -0
- package/js/modules/highlight.js +89 -0
- package/js/modules/navigation.js +104 -0
- package/js/modules/quantity.js +63 -0
- package/js/modules/rhythm.js +21 -0
- package/js/modules/table.js +109 -0
- package/js/modules/tabs.js +112 -0
- package/js/modules/toast.js +97 -0
- package/js/modules/tooltip.js +106 -0
- package/js/theme-init.js +8 -0
- package/package.json +71 -4
- package/scss/_blocks-only.scss +4 -0
- package/scss/_core.scss +7 -0
- package/scss/_index.scss +12 -0
- package/scss/_layers.scss +2 -0
- package/scss/_utilities-only.scss +4 -0
- package/scss/base/_code.scss +50 -0
- package/scss/base/_forms.scss +110 -0
- package/scss/base/_index.scss +7 -0
- package/scss/base/_interactive.scss +25 -0
- package/scss/base/_media.scss +28 -0
- package/scss/base/_reset.scss +53 -0
- package/scss/base/_tables.scss +49 -0
- package/scss/base/_typography.scss +232 -0
- package/scss/blocks/_accordion.scss +88 -0
- package/scss/blocks/_alert.scss +101 -0
- package/scss/blocks/_avatar.scss +104 -0
- package/scss/blocks/_badge.scss +72 -0
- package/scss/blocks/_breadcrumb.scss +65 -0
- package/scss/blocks/_button-group.scss +73 -0
- package/scss/blocks/_button.scss +212 -0
- package/scss/blocks/_card.scss +146 -0
- package/scss/blocks/_choice.scss +163 -0
- package/scss/blocks/_code.scss +157 -0
- package/scss/blocks/_data-list.scss +92 -0
- package/scss/blocks/_divider.scss +54 -0
- package/scss/blocks/_drawer.scss +160 -0
- package/scss/blocks/_dropdown.scss +167 -0
- package/scss/blocks/_empty-state.scss +61 -0
- package/scss/blocks/_field.scss +146 -0
- package/scss/blocks/_form.scss +87 -0
- package/scss/blocks/_gallery.scss +120 -0
- package/scss/blocks/_index.scss +49 -0
- package/scss/blocks/_input-group.scss +95 -0
- package/scss/blocks/_line-item.scss +105 -0
- package/scss/blocks/_list.scss +117 -0
- package/scss/blocks/_media.scss +105 -0
- package/scss/blocks/_menu.scss +191 -0
- package/scss/blocks/_meta.scss +59 -0
- package/scss/blocks/_modal.scss +156 -0
- package/scss/blocks/_navbar.scss +227 -0
- package/scss/blocks/_order-summary.scss +92 -0
- package/scss/blocks/_page-header.scss +88 -0
- package/scss/blocks/_pagination.scss +110 -0
- package/scss/blocks/_price.scss +88 -0
- package/scss/blocks/_product-card.scss +138 -0
- package/scss/blocks/_progress.scss +68 -0
- package/scss/blocks/_prose.scss +165 -0
- package/scss/blocks/_quantity.scss +73 -0
- package/scss/blocks/_quote.scss +71 -0
- package/scss/blocks/_rating.scss +50 -0
- package/scss/blocks/_site-footer.scss +135 -0
- package/scss/blocks/_skeleton.scss +63 -0
- package/scss/blocks/_spinner.scss +44 -0
- package/scss/blocks/_stat.scss +111 -0
- package/scss/blocks/_steps.scss +147 -0
- package/scss/blocks/_swatch.scss +117 -0
- package/scss/blocks/_table.scss +242 -0
- package/scss/blocks/_tabs.scss +168 -0
- package/scss/blocks/_tag.scss +72 -0
- package/scss/blocks/_timeline.scss +84 -0
- package/scss/blocks/_toast.scss +146 -0
- package/scss/blocks/_toolbar.scss +67 -0
- package/scss/blocks/_tooltip.scss +63 -0
- package/scss/helpers/_index.scss +2 -0
- package/scss/helpers/_rhythm.scss +64 -0
- package/scss/helpers/_typography.scss +4 -0
- package/scss/layout/_cluster.scss +43 -0
- package/scss/layout/_container.scss +26 -0
- package/scss/layout/_grid.scss +90 -0
- package/scss/layout/_index.scss +5 -0
- package/scss/layout/_section.scss +78 -0
- package/scss/layout/_shell.scss +87 -0
- package/scss/settings/_blocks.scss +3 -0
- package/scss/settings/_breakpoints.scss +7 -0
- package/scss/settings/_fonts.scss +5 -0
- package/scss/settings/_index.scss +7 -0
- package/scss/settings/_layout.scss +5 -0
- package/scss/settings/_prefix.scss +1 -0
- package/scss/settings/_rhythm.scss +6 -0
- package/scss/settings/_utilities.scss +2 -0
- package/scss/tokens/_borders.scss +16 -0
- package/scss/tokens/_elevation.scss +37 -0
- package/scss/tokens/_index.scss +8 -0
- package/scss/tokens/_layout.scss +17 -0
- package/scss/tokens/_motion.scss +17 -0
- package/scss/tokens/_palette.scss +50 -0
- package/scss/tokens/_roles.scss +90 -0
- package/scss/tokens/_space.scss +9 -0
- package/scss/tokens/_type.scss +57 -0
- package/scss/tools/_bem.scss +22 -0
- package/scss/tools/_index.scss +4 -0
- package/scss/tools/_rhythm.scss +26 -0
- package/scss/tools/_role-engine.scss +16 -0
- package/scss/tools/_utility-generator.scss +74 -0
- package/scss/utilities/_index.scss +8 -0
- package/scss/utilities/_map.scss +321 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { prefix } from '../config.js';
|
|
2
|
+
|
|
3
|
+
const ATTR = `data-${prefix}quantity`;
|
|
4
|
+
const DEC = `${prefix}quantity__decrement`;
|
|
5
|
+
const INC = `${prefix}quantity__increment`;
|
|
6
|
+
const INPUT = `${prefix}quantity__input`;
|
|
7
|
+
|
|
8
|
+
export function initQuantity() {
|
|
9
|
+
document.querySelectorAll(`[${ATTR}]`).forEach((root) => {
|
|
10
|
+
if (root._lnQuantity) return;
|
|
11
|
+
const q = new Quantity(root);
|
|
12
|
+
q.init();
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class Quantity {
|
|
17
|
+
constructor(root) {
|
|
18
|
+
this.root = root;
|
|
19
|
+
this.input = root.querySelector(`.${INPUT}`);
|
|
20
|
+
this.decBtn = root.querySelector(`.${DEC}`);
|
|
21
|
+
this.incBtn = root.querySelector(`.${INC}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
init() {
|
|
25
|
+
if (!this.input) return;
|
|
26
|
+
this.root._lnQuantity = true;
|
|
27
|
+
|
|
28
|
+
this.decBtn?.addEventListener('click', () => this.step(-1));
|
|
29
|
+
this.incBtn?.addEventListener('click', () => this.step(1));
|
|
30
|
+
this.input.addEventListener('change', () => this.clamp());
|
|
31
|
+
|
|
32
|
+
this.updateButtons();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get min() { return Number(this.input.min) || 0; }
|
|
36
|
+
get max() { return Number(this.input.max) || Infinity; }
|
|
37
|
+
get stepVal() { return Number(this.input.step) || 1; }
|
|
38
|
+
get value() { return Number(this.input.value) || 0; }
|
|
39
|
+
|
|
40
|
+
step(direction) {
|
|
41
|
+
const next = this.value + (this.stepVal * direction);
|
|
42
|
+
this.setValue(next);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
clamp() {
|
|
46
|
+
let val = this.value;
|
|
47
|
+
if (val < this.min) val = this.min;
|
|
48
|
+
if (val > this.max) val = this.max;
|
|
49
|
+
this.setValue(val);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setValue(val) {
|
|
53
|
+
val = Math.max(this.min, Math.min(val, this.max));
|
|
54
|
+
this.input.value = val;
|
|
55
|
+
this.input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
56
|
+
this.updateButtons();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
updateButtons() {
|
|
60
|
+
if (this.decBtn) this.decBtn.disabled = this.value <= this.min;
|
|
61
|
+
if (this.incBtn) this.incBtn.disabled = this.value >= this.max;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { prefix } from '../config.js';
|
|
2
|
+
|
|
3
|
+
const ATTR = `data-${prefix}grid-toggle`;
|
|
4
|
+
|
|
5
|
+
export function initRhythm() {
|
|
6
|
+
document.querySelectorAll(`[${ATTR}]`).forEach((trigger) => {
|
|
7
|
+
if (trigger._lnGridToggle) return;
|
|
8
|
+
trigger._lnGridToggle = true;
|
|
9
|
+
trigger.addEventListener('click', () => {
|
|
10
|
+
const selector = trigger.getAttribute(ATTR);
|
|
11
|
+
const target = selector
|
|
12
|
+
? document.querySelector(selector)
|
|
13
|
+
: trigger.parentElement;
|
|
14
|
+
|
|
15
|
+
if (!target) return;
|
|
16
|
+
|
|
17
|
+
const active = target.classList.toggle('show-grid');
|
|
18
|
+
trigger.setAttribute('aria-pressed', String(active));
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { prefix } from '../config.js';
|
|
2
|
+
|
|
3
|
+
const ATTR = `data-${prefix}table`;
|
|
4
|
+
const SORT = `${prefix}table__sort`;
|
|
5
|
+
const SELECT = `${prefix}table__select`;
|
|
6
|
+
|
|
7
|
+
export function initTable() {
|
|
8
|
+
document.querySelectorAll(`[${ATTR}]`).forEach((root) => {
|
|
9
|
+
if (root._lnTable) return;
|
|
10
|
+
const t = new Table(root);
|
|
11
|
+
t.init();
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class Table {
|
|
16
|
+
constructor(root) {
|
|
17
|
+
this.root = root;
|
|
18
|
+
this.table = root.querySelector('table') || root;
|
|
19
|
+
this.tbody = this.table.querySelector('tbody');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
init() {
|
|
23
|
+
this.root._lnTable = true;
|
|
24
|
+
this.initSort();
|
|
25
|
+
this.initSelectAll();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
initSort() {
|
|
29
|
+
this.root.querySelectorAll(`.${SORT}`).forEach((btn) => {
|
|
30
|
+
btn.addEventListener('click', () => {
|
|
31
|
+
const key = btn.getAttribute('data-sort-key');
|
|
32
|
+
if (!key || !this.tbody) return;
|
|
33
|
+
|
|
34
|
+
const current = btn.getAttribute('aria-sort');
|
|
35
|
+
const direction = current === 'ascending' ? 'descending' : 'ascending';
|
|
36
|
+
|
|
37
|
+
this.root.querySelectorAll(`.${SORT}`).forEach((b) => {
|
|
38
|
+
b.removeAttribute('aria-sort');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
btn.setAttribute('aria-sort', direction);
|
|
42
|
+
|
|
43
|
+
const colIndex = this.getColumnIndex(btn);
|
|
44
|
+
if (colIndex === -1) return;
|
|
45
|
+
|
|
46
|
+
const rows = [...this.tbody.querySelectorAll('tr')];
|
|
47
|
+
rows.sort((a, b) => {
|
|
48
|
+
const aVal = a.cells[colIndex]?.textContent.trim() ?? '';
|
|
49
|
+
const bVal = b.cells[colIndex]?.textContent.trim() ?? '';
|
|
50
|
+
const aNum = parseFloat(aVal);
|
|
51
|
+
const bNum = parseFloat(bVal);
|
|
52
|
+
|
|
53
|
+
let cmp;
|
|
54
|
+
if (!isNaN(aNum) && !isNaN(bNum)) {
|
|
55
|
+
cmp = aNum - bNum;
|
|
56
|
+
} else {
|
|
57
|
+
cmp = aVal.localeCompare(bVal);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return direction === 'ascending' ? cmp : -cmp;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
rows.forEach((row) => this.tbody.appendChild(row));
|
|
64
|
+
|
|
65
|
+
this.root.dispatchEvent(
|
|
66
|
+
new CustomEvent(`${prefix}table:sort`, {
|
|
67
|
+
bubbles: true,
|
|
68
|
+
detail: { key, direction }
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getColumnIndex(btn) {
|
|
76
|
+
const th = btn.closest('th');
|
|
77
|
+
if (!th) return -1;
|
|
78
|
+
return [...th.parentElement.children].indexOf(th);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
initSelectAll() {
|
|
82
|
+
const selectAll = this.root.querySelector(`th .${SELECT}`);
|
|
83
|
+
if (!selectAll || !this.tbody) return;
|
|
84
|
+
|
|
85
|
+
const rowCheckboxes = () => this.tbody.querySelectorAll(`.${SELECT}`);
|
|
86
|
+
|
|
87
|
+
selectAll.addEventListener('change', () => {
|
|
88
|
+
rowCheckboxes().forEach((cb) => {
|
|
89
|
+
cb.checked = selectAll.checked;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
this.root.dispatchEvent(
|
|
93
|
+
new CustomEvent(`${prefix}table:select`, {
|
|
94
|
+
bubbles: true,
|
|
95
|
+
detail: { all: selectAll.checked }
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
this.tbody.addEventListener('change', (e) => {
|
|
101
|
+
if (!e.target.classList.contains(SELECT)) return;
|
|
102
|
+
const boxes = [...rowCheckboxes()];
|
|
103
|
+
const allChecked = boxes.every((cb) => cb.checked);
|
|
104
|
+
const someChecked = boxes.some((cb) => cb.checked);
|
|
105
|
+
selectAll.checked = allChecked;
|
|
106
|
+
selectAll.indeterminate = someChecked && !allChecked;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { prefix } from '../config.js';
|
|
2
|
+
|
|
3
|
+
const TAB = `${prefix}tabs__tab`;
|
|
4
|
+
const PANEL = `${prefix}tabs__panel`;
|
|
5
|
+
const LIST = `${prefix}tabs__list`;
|
|
6
|
+
|
|
7
|
+
export function initTabs() {
|
|
8
|
+
document.querySelectorAll(`[data-${prefix}tabs]`).forEach((root) => {
|
|
9
|
+
if (root._lnTabs) return;
|
|
10
|
+
root._lnTabs = true;
|
|
11
|
+
const tabs = new Tabs(root);
|
|
12
|
+
tabs.init();
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class Tabs {
|
|
17
|
+
constructor(root) {
|
|
18
|
+
this.root = root;
|
|
19
|
+
this.tablist = root.querySelector(`.${LIST}`);
|
|
20
|
+
this.tabs = [...root.querySelectorAll(`.${TAB}`)];
|
|
21
|
+
this.panels = [...root.querySelectorAll(`.${PANEL}`)];
|
|
22
|
+
this.manual = root.dataset[`${prefix}tabs`] === 'manual' ||
|
|
23
|
+
root.getAttribute(`data-${prefix}tabs`) === 'manual';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
init() {
|
|
27
|
+
if (!this.tablist || this.tabs.length === 0) return;
|
|
28
|
+
|
|
29
|
+
this.tablist.setAttribute('role', 'tablist');
|
|
30
|
+
this.tabs.forEach((tab, i) => {
|
|
31
|
+
tab.setAttribute('role', 'tab');
|
|
32
|
+
const panelId = tab.getAttribute('aria-controls');
|
|
33
|
+
const panel = panelId ? document.getElementById(panelId) : this.panels[i];
|
|
34
|
+
if (panel) panel.setAttribute('role', 'tabpanel');
|
|
35
|
+
|
|
36
|
+
if (tab.getAttribute('aria-selected') !== 'true') {
|
|
37
|
+
tab.setAttribute('aria-selected', 'false');
|
|
38
|
+
tab.setAttribute('tabindex', '-1');
|
|
39
|
+
if (panel) panel.hidden = true;
|
|
40
|
+
} else {
|
|
41
|
+
tab.setAttribute('tabindex', '0');
|
|
42
|
+
if (panel) panel.hidden = false;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!this.tabs.some(t => t.getAttribute('aria-selected') === 'true')) {
|
|
47
|
+
this.activate(this.tabs[0]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.tablist.addEventListener('keydown', (e) => this.onKeydown(e));
|
|
51
|
+
this.tablist.addEventListener('click', (e) => {
|
|
52
|
+
const tab = e.target.closest(`.${TAB}`);
|
|
53
|
+
if (tab) this.activate(tab);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const hashAttr = `data-${prefix}tabs-hash`;
|
|
57
|
+
if (this.root.hasAttribute(hashAttr)) {
|
|
58
|
+
this.syncFromHash();
|
|
59
|
+
window.addEventListener('hashchange', () => this.syncFromHash());
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
activate(tab) {
|
|
64
|
+
this.tabs.forEach((t, i) => {
|
|
65
|
+
const isActive = t === tab;
|
|
66
|
+
t.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
|
67
|
+
t.setAttribute('tabindex', isActive ? '0' : '-1');
|
|
68
|
+
const panelId = t.getAttribute('aria-controls');
|
|
69
|
+
const panel = panelId ? document.getElementById(panelId) : this.panels[i];
|
|
70
|
+
if (panel) panel.hidden = !isActive;
|
|
71
|
+
});
|
|
72
|
+
tab.focus();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
onKeydown(e) {
|
|
76
|
+
const idx = this.tabs.indexOf(e.target);
|
|
77
|
+
if (idx === -1) return;
|
|
78
|
+
let next;
|
|
79
|
+
switch (e.key) {
|
|
80
|
+
case 'ArrowRight':
|
|
81
|
+
case 'ArrowDown':
|
|
82
|
+
next = this.tabs[(idx + 1) % this.tabs.length];
|
|
83
|
+
break;
|
|
84
|
+
case 'ArrowLeft':
|
|
85
|
+
case 'ArrowUp':
|
|
86
|
+
next = this.tabs[(idx - 1 + this.tabs.length) % this.tabs.length];
|
|
87
|
+
break;
|
|
88
|
+
case 'Home':
|
|
89
|
+
next = this.tabs[0];
|
|
90
|
+
break;
|
|
91
|
+
case 'End':
|
|
92
|
+
next = this.tabs[this.tabs.length - 1];
|
|
93
|
+
break;
|
|
94
|
+
default:
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
if (this.manual) {
|
|
99
|
+
next.focus();
|
|
100
|
+
next.setAttribute('tabindex', '0');
|
|
101
|
+
e.target.setAttribute('tabindex', '-1');
|
|
102
|
+
} else {
|
|
103
|
+
this.activate(next);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
syncFromHash() {
|
|
108
|
+
const hash = location.hash.slice(1);
|
|
109
|
+
const tab = this.tabs.find(t => t.getAttribute('aria-controls') === hash);
|
|
110
|
+
if (tab) this.activate(tab);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { prefix } from '../config.js';
|
|
2
|
+
|
|
3
|
+
const BLOCK = `${prefix}toast`;
|
|
4
|
+
const REGION = `${prefix}toast-region`;
|
|
5
|
+
let region = null;
|
|
6
|
+
|
|
7
|
+
export function initToast() {
|
|
8
|
+
if (!region) {
|
|
9
|
+
region = document.createElement('div');
|
|
10
|
+
region.className = `${REGION} ${REGION}--bottom-end`;
|
|
11
|
+
region.setAttribute('aria-live', 'polite');
|
|
12
|
+
region.setAttribute('aria-relevant', 'additions');
|
|
13
|
+
document.body.appendChild(region);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class Toast {
|
|
18
|
+
static show({ title, body, role, duration = 5000, solid = false, position } = {}) {
|
|
19
|
+
if (!region) initToast();
|
|
20
|
+
|
|
21
|
+
if (position) {
|
|
22
|
+
region.className = `${REGION} ${REGION}--${position}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const toast = document.createElement('div');
|
|
26
|
+
const classes = [BLOCK];
|
|
27
|
+
if (role) classes.push(`${BLOCK}--${role}`);
|
|
28
|
+
if (solid) classes.push(`${BLOCK}--solid`);
|
|
29
|
+
toast.className = classes.join(' ');
|
|
30
|
+
toast.setAttribute('role', 'alert');
|
|
31
|
+
toast.setAttribute(`data-${prefix}dismissible`, '');
|
|
32
|
+
|
|
33
|
+
let html = '';
|
|
34
|
+
|
|
35
|
+
html += `<div class="${BLOCK}__content">`;
|
|
36
|
+
if (title) html += `<div class="${BLOCK}__title">${escapeHtml(title)}</div>`;
|
|
37
|
+
if (body) html += `<div class="${BLOCK}__body">${escapeHtml(body)}</div>`;
|
|
38
|
+
html += '</div>';
|
|
39
|
+
|
|
40
|
+
html += `<button class="${BLOCK}__close" data-${prefix}dismiss aria-label="Close">×</button>`;
|
|
41
|
+
|
|
42
|
+
if (duration > 0) {
|
|
43
|
+
html += `<div class="${BLOCK}__timer" style="width: 100%"></div>`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
toast.innerHTML = html;
|
|
47
|
+
region.appendChild(toast);
|
|
48
|
+
|
|
49
|
+
const closeBtn = toast.querySelector(`.${BLOCK}__close`);
|
|
50
|
+
closeBtn.addEventListener('click', () => remove(toast));
|
|
51
|
+
|
|
52
|
+
if (duration > 0) {
|
|
53
|
+
const timer = toast.querySelector(`.${BLOCK}__timer`);
|
|
54
|
+
let remaining = duration;
|
|
55
|
+
let startTime = Date.now();
|
|
56
|
+
let paused = false;
|
|
57
|
+
|
|
58
|
+
const updateTimer = () => {
|
|
59
|
+
if (paused) return;
|
|
60
|
+
const elapsed = Date.now() - startTime;
|
|
61
|
+
const pct = Math.max(0, 1 - elapsed / duration);
|
|
62
|
+
if (timer) timer.style.width = `${pct * 100}%`;
|
|
63
|
+
|
|
64
|
+
if (pct <= 0) {
|
|
65
|
+
remove(toast);
|
|
66
|
+
} else {
|
|
67
|
+
requestAnimationFrame(updateTimer);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
requestAnimationFrame(updateTimer);
|
|
72
|
+
|
|
73
|
+
toast.addEventListener('mouseenter', () => {
|
|
74
|
+
paused = true;
|
|
75
|
+
remaining = Math.max(0, duration - (Date.now() - startTime));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
toast.addEventListener('mouseleave', () => {
|
|
79
|
+
paused = false;
|
|
80
|
+
startTime = Date.now() - (duration - remaining);
|
|
81
|
+
requestAnimationFrame(updateTimer);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return toast;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function remove(toast) {
|
|
90
|
+
toast.remove();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function escapeHtml(str) {
|
|
94
|
+
const div = document.createElement('div');
|
|
95
|
+
div.textContent = str;
|
|
96
|
+
return div.innerHTML;
|
|
97
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { prefix } from '../config.js';
|
|
2
|
+
|
|
3
|
+
const ATTR = `data-${prefix}tooltip`;
|
|
4
|
+
const BLOCK = `${prefix}tooltip`;
|
|
5
|
+
let idCounter = 0;
|
|
6
|
+
|
|
7
|
+
export function initTooltip() {
|
|
8
|
+
document.querySelectorAll(`[${ATTR}]`).forEach((trigger) => {
|
|
9
|
+
if (trigger._lnTooltip) return;
|
|
10
|
+
const t = new Tooltip(trigger);
|
|
11
|
+
t.init();
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class Tooltip {
|
|
16
|
+
constructor(trigger) {
|
|
17
|
+
this.trigger = trigger;
|
|
18
|
+
this.tooltip = null;
|
|
19
|
+
this.showTimeout = null;
|
|
20
|
+
this.id = `${BLOCK}-${++idCounter}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
init() {
|
|
24
|
+
this.trigger._lnTooltip = true;
|
|
25
|
+
this.trigger.addEventListener('mouseenter', () => this.scheduleShow());
|
|
26
|
+
this.trigger.addEventListener('focusin', () => this.scheduleShow());
|
|
27
|
+
this.trigger.addEventListener('mouseleave', () => this.hide());
|
|
28
|
+
this.trigger.addEventListener('focusout', () => this.hide());
|
|
29
|
+
this.trigger.addEventListener('keydown', (e) => {
|
|
30
|
+
if (e.key === 'Escape') this.hide();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
scheduleShow() {
|
|
35
|
+
if (this.showTimeout) return;
|
|
36
|
+
this.showTimeout = setTimeout(() => this.show(), 200);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
show() {
|
|
40
|
+
if (this.tooltip) return;
|
|
41
|
+
|
|
42
|
+
const text = this.trigger.getAttribute(ATTR);
|
|
43
|
+
if (!text) return;
|
|
44
|
+
|
|
45
|
+
const position = this.trigger.getAttribute(`${ATTR}-position`) || 'top';
|
|
46
|
+
|
|
47
|
+
this.tooltip = document.createElement('div');
|
|
48
|
+
this.tooltip.className = `${BLOCK} ${BLOCK}--${position}`;
|
|
49
|
+
this.tooltip.id = this.id;
|
|
50
|
+
this.tooltip.setAttribute('role', 'tooltip');
|
|
51
|
+
this.tooltip.textContent = text;
|
|
52
|
+
|
|
53
|
+
const arrow = document.createElement('div');
|
|
54
|
+
arrow.className = `${BLOCK}__arrow`;
|
|
55
|
+
this.tooltip.appendChild(arrow);
|
|
56
|
+
|
|
57
|
+
document.body.appendChild(this.tooltip);
|
|
58
|
+
this.trigger.setAttribute('aria-describedby', this.id);
|
|
59
|
+
|
|
60
|
+
this.position(position);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
hide() {
|
|
64
|
+
if (this.showTimeout) {
|
|
65
|
+
clearTimeout(this.showTimeout);
|
|
66
|
+
this.showTimeout = null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (this.tooltip) {
|
|
70
|
+
this.tooltip.remove();
|
|
71
|
+
this.tooltip = null;
|
|
72
|
+
this.trigger.removeAttribute('aria-describedby');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
position(placement) {
|
|
77
|
+
if (!this.tooltip) return;
|
|
78
|
+
|
|
79
|
+
const triggerRect = this.trigger.getBoundingClientRect();
|
|
80
|
+
const tooltipRect = this.tooltip.getBoundingClientRect();
|
|
81
|
+
const gap = 8;
|
|
82
|
+
|
|
83
|
+
let top, left;
|
|
84
|
+
|
|
85
|
+
switch (placement) {
|
|
86
|
+
case 'bottom':
|
|
87
|
+
top = triggerRect.bottom + gap;
|
|
88
|
+
left = triggerRect.left + (triggerRect.width - tooltipRect.width) / 2;
|
|
89
|
+
break;
|
|
90
|
+
case 'start':
|
|
91
|
+
top = triggerRect.top + (triggerRect.height - tooltipRect.height) / 2;
|
|
92
|
+
left = triggerRect.left - tooltipRect.width - gap;
|
|
93
|
+
break;
|
|
94
|
+
case 'end':
|
|
95
|
+
top = triggerRect.top + (triggerRect.height - tooltipRect.height) / 2;
|
|
96
|
+
left = triggerRect.right + gap;
|
|
97
|
+
break;
|
|
98
|
+
default: // top
|
|
99
|
+
top = triggerRect.top - tooltipRect.height - gap;
|
|
100
|
+
left = triggerRect.left + (triggerRect.width - tooltipRect.width) / 2;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.tooltip.style.top = `${top + window.scrollY}px`;
|
|
104
|
+
this.tooltip.style.left = `${left + window.scrollX}px`;
|
|
105
|
+
}
|
|
106
|
+
}
|
package/js/theme-init.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,73 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@collectivetrait/lineaui",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A lightweight SCSS and JavaScript component library built on BEM naming, an 8px vertical rhythm, and cascade layers.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"SCSS library",
|
|
7
|
+
"BEM",
|
|
8
|
+
"component library",
|
|
9
|
+
"vertical rhythm",
|
|
10
|
+
"SCSS framework",
|
|
11
|
+
"UI components",
|
|
12
|
+
"accessible CSS",
|
|
13
|
+
"lightweight CSS",
|
|
14
|
+
"cascade layers",
|
|
15
|
+
"SCSS"
|
|
16
|
+
],
|
|
17
|
+
"author": "Andrés Villarreal",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/collectivetrait/lineaui.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://lineaui.com",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/collectivetrait/lineaui/issues"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "dist/bundle.js",
|
|
29
|
+
"style": "dist/styles.css",
|
|
30
|
+
"sideEffects": [
|
|
31
|
+
"*.css",
|
|
32
|
+
"*.scss"
|
|
33
|
+
],
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"scss",
|
|
37
|
+
"js",
|
|
38
|
+
"config.json",
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"README.md",
|
|
41
|
+
"CHANGELOG.md",
|
|
42
|
+
"humans.txt"
|
|
43
|
+
],
|
|
44
|
+
"exports": {
|
|
45
|
+
".": "./dist/bundle.js",
|
|
46
|
+
"./dist/styles.css": "./dist/styles.css",
|
|
47
|
+
"./dist/core.css": "./dist/core.css",
|
|
48
|
+
"./dist/blocks.css": "./dist/blocks.css",
|
|
49
|
+
"./dist/utilities.css": "./dist/utilities.css",
|
|
50
|
+
"./dist/icons.css": "./dist/icons.css",
|
|
51
|
+
"./dist/icons-manifest.json": "./dist/icons-manifest.json",
|
|
52
|
+
"./dist/theme-init.js": "./dist/theme-init.js",
|
|
53
|
+
"./scss": "./scss/_index.scss"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "node build.mjs",
|
|
57
|
+
"dev": "node build.mjs --watch",
|
|
58
|
+
"lint": "eslint && stylelint \"scss/**/*.scss\"",
|
|
59
|
+
"lint:js": "eslint",
|
|
60
|
+
"lint:scss": "stylelint \"scss/**/*.scss\"",
|
|
61
|
+
"prepublishOnly": "npm run build"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@eslint/js": "^10.0.1",
|
|
65
|
+
"cssnano": "^7.0.0",
|
|
66
|
+
"esbuild": "^0.24.0",
|
|
67
|
+
"eslint": "^10.12.0",
|
|
68
|
+
"postcss": "^8.4.0",
|
|
69
|
+
"sass": "^1.69.5",
|
|
70
|
+
"stylelint": "^17.16.0",
|
|
71
|
+
"stylelint-config-standard-scss": "^17.0.0"
|
|
72
|
+
}
|
|
73
|
+
}
|
package/scss/_core.scss
ADDED
package/scss/_index.scss
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @use rules must be first in Sass, then @forward, then CSS rules.
|
|
2
|
+
@use 'sass:map';
|
|
3
|
+
@use 'settings' as s;
|
|
4
|
+
|
|
5
|
+
// _layers.scss emits the @layer order declaration first in the CSS output.
|
|
6
|
+
@forward 'layers';
|
|
7
|
+
@forward 'tokens';
|
|
8
|
+
@forward 'base';
|
|
9
|
+
@forward 'layout';
|
|
10
|
+
@forward 'blocks';
|
|
11
|
+
@forward 'helpers';
|
|
12
|
+
@forward 'utilities';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
@use '../tools/rhythm' as r;
|
|
2
|
+
|
|
3
|
+
@layer base {
|
|
4
|
+
:is(code, kbd, samp, var) {
|
|
5
|
+
// Relative to parent so it scales in prose.
|
|
6
|
+
font: 0.875em var(--f-mono);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Inline code
|
|
10
|
+
:not(pre) > code {
|
|
11
|
+
background-color: var(--surface-sunken);
|
|
12
|
+
border-radius: var(--r-sm);
|
|
13
|
+
padding-inline: var(--s-h);
|
|
14
|
+
font-size: var(--t-code-s);
|
|
15
|
+
line-height: 1rem;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
kbd {
|
|
19
|
+
background-color: var(--surface-raised);
|
|
20
|
+
border: 1px solid var(--border);
|
|
21
|
+
border-radius: var(--r-sm);
|
|
22
|
+
padding: 0.1em var(--s-h);
|
|
23
|
+
box-shadow: 0 1px 0 var(--border);
|
|
24
|
+
font-size: var(--t-body-sm-s);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
pre {
|
|
28
|
+
font: var(--t-code-s)/var(--t-code-lh) var(--f-mono);
|
|
29
|
+
background-color: var(--surface-sunken);
|
|
30
|
+
border-radius: var(--r-xs);
|
|
31
|
+
overflow-x: auto;
|
|
32
|
+
tab-size: 2;
|
|
33
|
+
margin-bottom: r.lines(1);
|
|
34
|
+
|
|
35
|
+
// Padding lives on code, not pre.
|
|
36
|
+
// This keeps highlighted code (where .hljs is on <code>) and plain
|
|
37
|
+
// specimen code visually identical.
|
|
38
|
+
code {
|
|
39
|
+
display: block;
|
|
40
|
+
padding: var(--s3);
|
|
41
|
+
font: inherit;
|
|
42
|
+
background: none;
|
|
43
|
+
border-radius: 0;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pre:last-child {
|
|
48
|
+
margin-bottom: 0;
|
|
49
|
+
}
|
|
50
|
+
}
|