@apliteni/apliteni-ui 0.6.1 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Apliteni shared design system & UI kit — tokens, components and the deck theme, framework-agnostic (HTML + CSS).",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -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
@@ -12,6 +12,7 @@
12
12
  @import "./styles/card.css";
13
13
  @import "./styles/badge.css";
14
14
  @import "./styles/segmented.css";
15
+ @import "./styles/tabs.css";
15
16
  @import "./styles/input.css";
16
17
  @import "./styles/dropdown.css";
17
18
  @import "./styles/nav.css";
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,
@@ -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; }