@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.6.1",
3
+ "version": "0.7.1",
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,
@@ -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
- .ui-table--hover tbody tr:hover { background: var(--surface-2); }
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; }