@apliteni/apliteni-ui 0.6.1 → 0.7.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.
- package/package.json +1 -1
- package/src/components/tabs.js +72 -0
- package/src/index.css +1 -0
- package/src/index.js +1 -0
- package/src/inline.js +2 -0
- package/src/styles/table.css +18 -1
- package/src/styles/tabs.css +28 -0
package/package.json
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Tabs — an accessible tablist + panels, as a framework-agnostic HTML string.
|
|
2
|
+
// Render with tabs(), then wire behaviour once after mount with initTabs().
|
|
3
|
+
//
|
|
4
|
+
// root.innerHTML = tabs({
|
|
5
|
+
// name: 'account', active: 0, ariaLabel: 'Account sections',
|
|
6
|
+
// items: [
|
|
7
|
+
// { label: 'Overview', panel: '<p>…</p>' },
|
|
8
|
+
// { label: 'Activity', panel: '<p>…</p>' },
|
|
9
|
+
// { label: 'Settings', panel: '<p>…</p>' },
|
|
10
|
+
// ],
|
|
11
|
+
// });
|
|
12
|
+
// initTabs(root);
|
|
13
|
+
//
|
|
14
|
+
// Follows the WAI-ARIA tabs pattern: roving tabindex, ArrowLeft/Right + Home/End,
|
|
15
|
+
// aria-selected, and aria-controls / aria-labelledby wiring. `name` must be unique
|
|
16
|
+
// per tabs instance on a page (it seeds the tab/panel ids).
|
|
17
|
+
|
|
18
|
+
export function tabs({ items = [], active = 0, name = 'tabs', ariaLabel = 'Tabs', className = '' } = {}) {
|
|
19
|
+
const cls = ['ui-tabs', className].filter(Boolean).join(' ');
|
|
20
|
+
const list = items
|
|
21
|
+
.map((it, i) => {
|
|
22
|
+
const on = i === active;
|
|
23
|
+
return `<button class="ui-tabs__tab${on ? ' is-active' : ''}" role="tab"`
|
|
24
|
+
+ ` id="${name}-tab-${i}" aria-controls="${name}-panel-${i}"`
|
|
25
|
+
+ ` aria-selected="${on ? 'true' : 'false'}" tabindex="${on ? '0' : '-1'}">${it.label}</button>`;
|
|
26
|
+
})
|
|
27
|
+
.join('');
|
|
28
|
+
const panels = items
|
|
29
|
+
.map((it, i) => {
|
|
30
|
+
const on = i === active;
|
|
31
|
+
return `<div class="ui-tabs__panel" role="tabpanel" id="${name}-panel-${i}"`
|
|
32
|
+
+ ` aria-labelledby="${name}-tab-${i}"${on ? '' : ' hidden'}>${it.panel || ''}</div>`;
|
|
33
|
+
})
|
|
34
|
+
.join('');
|
|
35
|
+
return `<div class="${cls}" data-tabs>`
|
|
36
|
+
+ `<div class="ui-tabs__list" role="tablist" aria-label="${ariaLabel}">${list}</div>`
|
|
37
|
+
+ `<div class="ui-tabs__panels">${panels}</div>`
|
|
38
|
+
+ `</div>`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Wire click + keyboard for every [data-tabs] under `root`. Idempotent-safe to
|
|
42
|
+
// call once after the markup mounts. No-op off-DOM (SSR / node --test).
|
|
43
|
+
export function initTabs(root) {
|
|
44
|
+
if (typeof document === 'undefined') return;
|
|
45
|
+
const scope = root || document;
|
|
46
|
+
scope.querySelectorAll('[data-tabs]').forEach((el) => {
|
|
47
|
+
const tabEls = Array.prototype.slice.call(el.querySelectorAll('[role="tab"]'));
|
|
48
|
+
const panelFor = (t) => el.querySelector('[id="' + t.getAttribute('aria-controls') + '"]');
|
|
49
|
+
const select = (i, focus) => {
|
|
50
|
+
tabEls.forEach((t, j) => {
|
|
51
|
+
const on = j === i;
|
|
52
|
+
t.classList.toggle('is-active', on);
|
|
53
|
+
t.setAttribute('aria-selected', on ? 'true' : 'false');
|
|
54
|
+
t.tabIndex = on ? 0 : -1;
|
|
55
|
+
const p = panelFor(t);
|
|
56
|
+
if (p) p.hidden = !on;
|
|
57
|
+
});
|
|
58
|
+
if (focus && tabEls[i]) tabEls[i].focus();
|
|
59
|
+
};
|
|
60
|
+
tabEls.forEach((t, i) => {
|
|
61
|
+
t.addEventListener('click', () => select(i));
|
|
62
|
+
t.addEventListener('keydown', (e) => {
|
|
63
|
+
let n = null;
|
|
64
|
+
if (e.key === 'ArrowRight') n = (i + 1) % tabEls.length;
|
|
65
|
+
else if (e.key === 'ArrowLeft') n = (i - 1 + tabEls.length) % tabEls.length;
|
|
66
|
+
else if (e.key === 'Home') n = 0;
|
|
67
|
+
else if (e.key === 'End') n = tabEls.length - 1;
|
|
68
|
+
if (n !== null) { e.preventDefault(); select(n, true); }
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
package/src/index.css
CHANGED
package/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Styles ship separately: `import 'apliteni-ui/css'`.
|
|
3
3
|
export * from './components/index.js';
|
|
4
4
|
export * from './components/dropdown.js';
|
|
5
|
+
export * from './components/tabs.js';
|
|
5
6
|
export * from './components/nav.js';
|
|
6
7
|
export * from './components/drawer.js';
|
|
7
8
|
export * from './components/topbar.js';
|
package/src/inline.js
CHANGED
|
@@ -33,6 +33,7 @@ export const styles = {
|
|
|
33
33
|
card: read('styles/card.css'),
|
|
34
34
|
badge: read('styles/badge.css'),
|
|
35
35
|
segmented: read('styles/segmented.css'),
|
|
36
|
+
tabs: read('styles/tabs.css'),
|
|
36
37
|
input: read('styles/input.css'),
|
|
37
38
|
dropdown: read('styles/dropdown.css'),
|
|
38
39
|
nav: read('styles/nav.css'),
|
|
@@ -56,6 +57,7 @@ export const cssText = [
|
|
|
56
57
|
styles.card,
|
|
57
58
|
styles.badge,
|
|
58
59
|
styles.segmented,
|
|
60
|
+
styles.tabs,
|
|
59
61
|
styles.input,
|
|
60
62
|
styles.dropdown,
|
|
61
63
|
styles.nav,
|
package/src/styles/table.css
CHANGED
|
@@ -32,7 +32,24 @@
|
|
|
32
32
|
.ui-table tr.is-dead td { color: var(--muted); }
|
|
33
33
|
.ui-table tr.is-dead td.ui-table__title { text-decoration: line-through; }
|
|
34
34
|
.ui-table tbody tr { transition: background var(--dur-fast) var(--ease); }
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
/* Row hover: an inset, rounded fill so the highlight never sits flush against a
|
|
37
|
+
* container's border (issue #71 — the full-bleed rectangle collided with a card's
|
|
38
|
+
* left edge). Separate border model with zero spacing keeps the row separators
|
|
39
|
+
* continuous while letting the row ends round; table padding (which only applies
|
|
40
|
+
* in the separate model) insets the rows so the fill clears the container edge.
|
|
41
|
+
* Zebra tables keep the original full-bleed tint (finance ledger), so they're
|
|
42
|
+
* excluded here — their hover rule lives further down. */
|
|
43
|
+
.ui-table--hover:not(.ui-table--zebra) {
|
|
44
|
+
border-collapse: separate;
|
|
45
|
+
border-spacing: 0;
|
|
46
|
+
padding: 0 6px;
|
|
47
|
+
}
|
|
48
|
+
.ui-table--hover:not(.ui-table--zebra) tbody tr:hover td { background: var(--surface-2); }
|
|
49
|
+
.ui-table--hover:not(.ui-table--zebra) tbody tr:hover td:first-child {
|
|
50
|
+
border-top-left-radius: var(--radius-sm); border-bottom-left-radius: var(--radius-sm); }
|
|
51
|
+
.ui-table--hover:not(.ui-table--zebra) tbody tr:hover td:last-child {
|
|
52
|
+
border-top-right-radius: var(--radius-sm); border-bottom-right-radius: var(--radius-sm); }
|
|
36
53
|
|
|
37
54
|
/* ── Data-table modifiers (composable) ──────────────────────────────────────
|
|
38
55
|
* Compose for a dense financial ledger: .ui-table--dense --zebra --hover.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* Tabs — .ui-tabs
|
|
3
|
+
* A tablist with an underline indicator + panels. Markup comes from tabs() in
|
|
4
|
+
* components/tabs.js; wire it with initTabs(). Colours read the accent tokens.
|
|
5
|
+
* ========================================================================== */
|
|
6
|
+
.ui-tabs__list {
|
|
7
|
+
display: flex;
|
|
8
|
+
gap: 2px;
|
|
9
|
+
border-bottom: 1px solid var(--border);
|
|
10
|
+
}
|
|
11
|
+
.ui-tabs__tab {
|
|
12
|
+
appearance: none;
|
|
13
|
+
background: none;
|
|
14
|
+
border: 0;
|
|
15
|
+
cursor: pointer;
|
|
16
|
+
font: 600 14px/1 var(--font-sans);
|
|
17
|
+
color: var(--dim);
|
|
18
|
+
padding: 11px 14px;
|
|
19
|
+
border-bottom: 2px solid transparent;
|
|
20
|
+
margin-bottom: -1px;
|
|
21
|
+
transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
|
|
22
|
+
}
|
|
23
|
+
.ui-tabs__tab:hover { color: var(--text); }
|
|
24
|
+
.ui-tabs__tab.is-active { color: var(--strong); border-bottom-color: var(--accent); }
|
|
25
|
+
.ui-tabs__tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 7px 7px 0 0; }
|
|
26
|
+
|
|
27
|
+
.ui-tabs__panels { padding-top: 20px; }
|
|
28
|
+
.ui-tabs__panel[hidden] { display: none; }
|