@apliteni/apliteni-ui 0.4.0 → 0.5.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 +8 -3
- package/src/assets/brand.generated/apliteni-logo-dark.svg +27 -0
- package/src/assets/brand.generated/apliteni-logo.svg +27 -0
- package/src/assets/brand.generated/apliteni-mark.svg +19 -0
- package/src/assets/brand.generated/index.js +5 -0
- package/src/assets/icons.js +106 -24
- package/src/assets/illustrations.js +68 -0
- package/src/components/drawer.js +200 -0
- package/src/components/dropdown.js +229 -0
- package/src/components/feedback.js +6 -0
- package/src/components/footer.js +95 -0
- package/src/components/index.js +46 -4
- package/src/components/nav.js +190 -0
- package/src/components/success.js +126 -0
- package/src/components/toasts.js +94 -0
- package/src/components/topbar.js +16 -15
- package/src/index.css +8 -0
- package/src/index.js +5 -0
- package/src/inline.js +19 -3
- package/src/motion.js +68 -0
- package/src/styles/button.css +40 -11
- package/src/styles/callout.css +79 -12
- package/src/styles/drawer.css +166 -0
- package/src/styles/dropdown.css +133 -0
- package/src/styles/empty.css +84 -0
- package/src/styles/feedback.css +1 -1
- package/src/styles/footer.css +123 -0
- package/src/styles/motion.css +166 -0
- package/src/styles/nav.css +218 -0
- package/src/styles/success.css +189 -0
- package/src/styles/table.css +2 -15
- package/src/tokens/brand.generated.css +121 -0
- package/src/tokens/tokens.css +18 -5
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Success / confirmation surface — the emotional high point of a flow, done
|
|
2
|
+
// with craft. One factory, three layouts and three backdrops, an SVG check
|
|
3
|
+
// that draws itself in, optional confetti and an optional auto-redirect
|
|
4
|
+
// countdown. Every motion path is reduced-motion safe (static check, no burst,
|
|
5
|
+
// no confetti, no sweep). Accent-aware via the kit tokens; the success mark
|
|
6
|
+
// stays on the --green family. Styles ship in styles/success.css.
|
|
7
|
+
//
|
|
8
|
+
// container.innerHTML = success({
|
|
9
|
+
// title: 'Feedback sent',
|
|
10
|
+
// body: 'It goes straight to the strategy owner.',
|
|
11
|
+
// actions: [
|
|
12
|
+
// { label: 'Back to strategy', variant: 'primary', icon: 'compass' },
|
|
13
|
+
// { label: 'Send another', variant: 'ghost' },
|
|
14
|
+
// ],
|
|
15
|
+
// });
|
|
16
|
+
//
|
|
17
|
+
// The check animation is pure CSS, so string-rendered markup animates on its
|
|
18
|
+
// own once mounted. A live countdown (ticking numbers + redirect) is opt-in via
|
|
19
|
+
// wireSuccess(); the markup alone shows the ring sweep + a static number.
|
|
20
|
+
import { esc, button } from './index.js';
|
|
21
|
+
|
|
22
|
+
// Self-drawing check: a faint track disc, a filled accent disc that springs in,
|
|
23
|
+
// the tick that strokes on via dash-offset, and a burst ring that expands and
|
|
24
|
+
// fades. viewBox 0 0 52 52; the tick path length is ~34 (dasharray tuned to it).
|
|
25
|
+
export function successCheck() {
|
|
26
|
+
// width/height attributes keep the base svg:not([width]) fallback from
|
|
27
|
+
// sizing us; the real size is set per-layout in success.css.
|
|
28
|
+
return `<svg class="ui-sx__check" width="52" height="52" viewBox="0 0 52 52" aria-hidden="true" focusable="false">
|
|
29
|
+
<circle class="ui-sx__ring" cx="26" cy="26" r="24"/>
|
|
30
|
+
<circle class="ui-sx__disc" cx="26" cy="26" r="24"/>
|
|
31
|
+
<path class="ui-sx__tick" d="M15 27l7.5 7.5L37 18"/>
|
|
32
|
+
</svg>`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// A deterministic confetti field (opt-in). Decorative + aria-hidden; each piece
|
|
36
|
+
// carries its own drift/rotation/colour via custom props so the CSS can scatter
|
|
37
|
+
// them without inline keyframes. Deterministic so string renders stay stable
|
|
38
|
+
// (and the a11y snapshot is reproducible).
|
|
39
|
+
const CONFETTI = [
|
|
40
|
+
{ x: 8, d: 0.00, r: -24, t: 'a', s: 1.0 }, { x: 20, d: 0.14, r: 40, t: 'g', s: 0.8 },
|
|
41
|
+
{ x: 31, d: 0.06, r: 12, t: 'c', s: 1.1 }, { x: 42, d: 0.20, r: -52, t: 'p', s: 0.9 },
|
|
42
|
+
{ x: 50, d: 0.02, r: 28, t: 'a', s: 1.0 }, { x: 58, d: 0.18, r: -16, t: 'k', s: 0.75 },
|
|
43
|
+
{ x: 67, d: 0.09, r: 60, t: 'g', s: 1.05 },{ x: 76, d: 0.24, r: -36, t: 'c', s: 0.85 },
|
|
44
|
+
{ x: 85, d: 0.05, r: 20, t: 'a', s: 1.0 }, { x: 92, d: 0.16, r: -48, t: 'p', s: 0.9 },
|
|
45
|
+
{ x: 14, d: 0.30, r: 44, t: 'k', s: 0.8 }, { x: 37, d: 0.34, r: -28, t: 'g', s: 1.0 },
|
|
46
|
+
{ x: 62, d: 0.28, r: 52, t: 'a', s: 0.9 }, { x: 80, d: 0.36, r: -20, t: 'c', s: 1.05 },
|
|
47
|
+
];
|
|
48
|
+
function confettiField() {
|
|
49
|
+
const pieces = CONFETTI.map((p) =>
|
|
50
|
+
`<i class="ui-sx__piece ui-sx__piece--${p.t}" style="left:${p.x}%;--sx-d:${p.d}s;--sx-r:${p.r}deg;--sx-s:${p.s}"></i>`,
|
|
51
|
+
).join('');
|
|
52
|
+
return `<div class="ui-sx__confetti" aria-hidden="true">${pieces}</div>`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Optional auto-redirect countdown. Markup-only here (a conic ring sweeps and a
|
|
56
|
+
// static number shows); wireSuccess() turns it into a live ticking counter.
|
|
57
|
+
function countdownEl({ seconds = 5, label = 'Redirecting' } = {}) {
|
|
58
|
+
return `<div class="ui-sx__count" data-sx-count style="--sx-secs:${seconds}s">
|
|
59
|
+
<span class="ui-sx__count-ring" aria-hidden="true"></span>
|
|
60
|
+
<span class="ui-sx__count-text">${esc(label)} in <b data-sx-num>${seconds}</b>s</span>
|
|
61
|
+
</div>`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function success({
|
|
65
|
+
layout = 'hero', // 'hero' | 'split' | 'compact'
|
|
66
|
+
backdrop = 'aurora', // 'aurora' | 'glow' | 'flat'
|
|
67
|
+
eyebrow = '',
|
|
68
|
+
title = 'All done',
|
|
69
|
+
body = '',
|
|
70
|
+
actions = [], // [{ label, variant, href, icon, iconRight, size }]
|
|
71
|
+
confetti = false, // opt-in particle burst (reduced-motion safe)
|
|
72
|
+
countdown = null, // { seconds, label } | null
|
|
73
|
+
className = '',
|
|
74
|
+
} = {}) {
|
|
75
|
+
const cls = ['ui-sx', `ui-sx--${layout}`, `ui-sx--bd-${backdrop}`, confetti && 'ui-sx--confetti', className]
|
|
76
|
+
.filter(Boolean).join(' ');
|
|
77
|
+
|
|
78
|
+
const bd = backdrop === 'aurora'
|
|
79
|
+
? `<div class="ui-sx__aurora" aria-hidden="true">
|
|
80
|
+
<span class="ui-sx__glow ui-sx__glow--a"></span>
|
|
81
|
+
<span class="ui-sx__glow ui-sx__glow--b"></span>
|
|
82
|
+
</div>`
|
|
83
|
+
: backdrop === 'glow'
|
|
84
|
+
? `<span class="ui-glow ui-glow--green ui-sx__bg-glow" aria-hidden="true"></span>`
|
|
85
|
+
: '';
|
|
86
|
+
|
|
87
|
+
const eyebrowEl = eyebrow ? `<div class="ui-sx__eyebrow">${esc(eyebrow)}</div>` : '';
|
|
88
|
+
const bodyEl = body ? `<p class="ui-sx__body">${esc(body)}</p>` : '';
|
|
89
|
+
const actionsEl = actions.length
|
|
90
|
+
? `<div class="ui-sx__actions">${actions.map((a) => button({ size: 'md', ...a })).join('')}</div>`
|
|
91
|
+
: '';
|
|
92
|
+
const countEl = countdown ? countdownEl(countdown) : '';
|
|
93
|
+
|
|
94
|
+
return `<div class="${cls}" role="status" aria-live="polite">
|
|
95
|
+
${bd}
|
|
96
|
+
${confetti ? confettiField() : ''}
|
|
97
|
+
<div class="ui-sx__inner">
|
|
98
|
+
<div class="ui-sx__visual">${successCheck()}</div>
|
|
99
|
+
<div class="ui-sx__content">
|
|
100
|
+
${eyebrowEl}
|
|
101
|
+
<h3 class="ui-sx__title">${esc(title)}</h3>
|
|
102
|
+
${bodyEl}
|
|
103
|
+
${actionsEl}
|
|
104
|
+
${countEl}
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Live countdown wiring (opt-in). Ticks the number down once a second and calls
|
|
111
|
+
// onDone() at zero — e.g. to navigate. Respects reduced-motion only for the
|
|
112
|
+
// visual sweep (owned by CSS); the counter itself is content, so it still runs.
|
|
113
|
+
// Returns a stop() to cancel (unmount / user interaction).
|
|
114
|
+
// const stop = wireSuccess(el, { onDone: () => location.assign('/strategy') });
|
|
115
|
+
export function wireSuccess(root, { onDone } = {}) {
|
|
116
|
+
const box = root && root.querySelector('[data-sx-count]');
|
|
117
|
+
if (!box) return () => {};
|
|
118
|
+
const numEl = box.querySelector('[data-sx-num]');
|
|
119
|
+
let n = parseInt(box.style.getPropertyValue('--sx-secs'), 10) || parseInt(numEl?.textContent, 10) || 5;
|
|
120
|
+
const id = setInterval(() => {
|
|
121
|
+
n -= 1;
|
|
122
|
+
if (numEl) numEl.textContent = String(Math.max(0, n));
|
|
123
|
+
if (n <= 0) { clearInterval(id); if (typeof onDone === 'function') onDone(); }
|
|
124
|
+
}, 1000);
|
|
125
|
+
return () => clearInterval(id);
|
|
126
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Toast stack controller — entrance/exit, auto-dismiss timers, swipe-to-dismiss.
|
|
2
|
+
//
|
|
3
|
+
// The visual markup comes from toast() in index.js; this wires behaviour onto a
|
|
4
|
+
// container of them, or lets you push new ones onto a live stack:
|
|
5
|
+
//
|
|
6
|
+
// import { toast } from '@apliteni/apliteni-ui';
|
|
7
|
+
// import { wireToastStack, pushToast } from '@apliteni/apliteni-ui/toasts';
|
|
8
|
+
// stack.innerHTML = toast({ variant: 'info', title: 'Saved', timer: 5 });
|
|
9
|
+
// wireToastStack(stack);
|
|
10
|
+
// pushToast(stack, { variant: 'success', title: 'Done', timer: 5 });
|
|
11
|
+
//
|
|
12
|
+
// The kit owns the animation + interaction; your app decides when to push and
|
|
13
|
+
// what each toast says. Reduced-motion is respected (no slide, instant remove).
|
|
14
|
+
import { toast } from './index.js';
|
|
15
|
+
|
|
16
|
+
const reduceMotion = () =>
|
|
17
|
+
typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
18
|
+
|
|
19
|
+
// Slide-and-fade a toast out, then remove it. Idempotent.
|
|
20
|
+
export function dismissToast(el) {
|
|
21
|
+
if (!el || el.dataset.leaving) return;
|
|
22
|
+
el.dataset.leaving = '1';
|
|
23
|
+
const remove = () => el.remove();
|
|
24
|
+
if (reduceMotion()) return remove();
|
|
25
|
+
el.classList.add('is-leaving');
|
|
26
|
+
el.addEventListener('animationend', remove, { once: true });
|
|
27
|
+
setTimeout(remove, 260); // fallback if animationend never fires
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Wire one toast: close button, auto-dismiss timer (pauses on hover),
|
|
31
|
+
// swipe-to-dismiss via pointer drag.
|
|
32
|
+
function wireToast(el) {
|
|
33
|
+
if (el.dataset.wired) return;
|
|
34
|
+
el.dataset.wired = '1';
|
|
35
|
+
|
|
36
|
+
el.querySelector('.ui-toast__close')?.addEventListener('click', () => dismissToast(el));
|
|
37
|
+
|
|
38
|
+
const bar = el.querySelector('[data-toast-timer]');
|
|
39
|
+
if (bar && !reduceMotion()) {
|
|
40
|
+
bar.classList.add('is-running');
|
|
41
|
+
const dur = parseFloat(getComputedStyle(el).getPropertyValue('--toast-dur')) || 5;
|
|
42
|
+
let timer = setTimeout(() => dismissToast(el), dur * 1000);
|
|
43
|
+
el.addEventListener('mouseenter', () => { clearTimeout(timer); bar.style.animationPlayState = 'paused'; });
|
|
44
|
+
el.addEventListener('mouseleave', () => {
|
|
45
|
+
bar.style.animationPlayState = 'running';
|
|
46
|
+
const left = (parseFloat(getComputedStyle(bar).transform.split(',')[0].slice(7)) || 1);
|
|
47
|
+
timer = setTimeout(() => dismissToast(el), dur * 1000 * left);
|
|
48
|
+
});
|
|
49
|
+
} else if (bar && reduceMotion()) {
|
|
50
|
+
// No animated bar under reduced motion, but still auto-dismiss on time.
|
|
51
|
+
const dur = parseFloat(getComputedStyle(el).getPropertyValue('--toast-dur')) || 5;
|
|
52
|
+
setTimeout(() => dismissToast(el), dur * 1000);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Swipe-to-dismiss (ignore drags that start on a button).
|
|
56
|
+
let x0 = null;
|
|
57
|
+
el.addEventListener('pointerdown', (e) => {
|
|
58
|
+
if (e.target.closest('button')) return;
|
|
59
|
+
x0 = e.clientX; el.setPointerCapture(e.pointerId); el.style.transition = 'none';
|
|
60
|
+
});
|
|
61
|
+
el.addEventListener('pointermove', (e) => {
|
|
62
|
+
if (x0 == null) return;
|
|
63
|
+
const dx = e.clientX - x0;
|
|
64
|
+
el.style.transform = `translateX(${dx}px)`;
|
|
65
|
+
el.style.opacity = String(Math.max(0, 1 - Math.abs(dx) / 240));
|
|
66
|
+
});
|
|
67
|
+
const settle = (e) => {
|
|
68
|
+
if (x0 == null) return;
|
|
69
|
+
const dx = e.clientX - x0; x0 = null;
|
|
70
|
+
if (Math.abs(dx) > 90) return dismissToast(el);
|
|
71
|
+
el.style.transition = 'transform 0.18s ease, opacity 0.18s ease';
|
|
72
|
+
el.style.transform = ''; el.style.opacity = '';
|
|
73
|
+
};
|
|
74
|
+
el.addEventListener('pointerup', settle);
|
|
75
|
+
el.addEventListener('pointercancel', settle);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Wire every toast currently inside a container (string selector or element).
|
|
79
|
+
export function wireToastStack(container) {
|
|
80
|
+
const root = typeof container === 'string' ? document.querySelector(container) : container;
|
|
81
|
+
if (!root) return null;
|
|
82
|
+
root.querySelectorAll('.ui-toast').forEach(wireToast);
|
|
83
|
+
return root;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Build a toast from opts, prepend it to a live stack (newest on top), wire it.
|
|
87
|
+
export function pushToast(container, opts = {}) {
|
|
88
|
+
const root = typeof container === 'string' ? document.querySelector(container) : container;
|
|
89
|
+
if (!root) return null;
|
|
90
|
+
root.insertAdjacentHTML('afterbegin', toast(opts));
|
|
91
|
+
const el = root.firstElementChild;
|
|
92
|
+
wireToast(el);
|
|
93
|
+
return el;
|
|
94
|
+
}
|
package/src/components/topbar.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { brand } from '../assets/brand.js';
|
|
4
4
|
import { icon, sun, moon } from '../assets/icons.js';
|
|
5
5
|
import { esc } from './index.js';
|
|
6
|
+
import { wireDropdown } from './dropdown.js';
|
|
6
7
|
|
|
7
8
|
const THEME_KEY = 'apliteni-strategy-theme';
|
|
8
9
|
|
|
@@ -16,15 +17,18 @@ export function deckTextSwitch(active = 'deck') {
|
|
|
16
17
|
`<a${active === 'text' ? ' class="cur" aria-current="page"' : ''} href="#text">Text</a></div>`;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
// Thin consumer of the shared dropdown wiring (wireDropdown): keeps its own
|
|
21
|
+
// bespoke .vsw/.vopt classes for a pixel-identical look, but emits the generic
|
|
22
|
+
// [data-dropdown] hooks so there's ONE open/close/keyboard implementation.
|
|
19
23
|
export function versionSwitcher(versions = [], activeIdx = 0) {
|
|
20
24
|
const cur = versions[activeIdx]?.label || '';
|
|
21
25
|
const opts = versions.map((v, i) =>
|
|
22
|
-
`<div class="vopt" role="option" aria-selected="${i === activeIdx}" data-active="${i === activeIdx ? '1' : '0'}">` +
|
|
26
|
+
`<div class="vopt" role="option" data-dd-item tabindex="-1" aria-selected="${i === activeIdx}" data-active="${i === activeIdx ? '1' : '0'}">` +
|
|
23
27
|
`<span><div class="vname">${v.label}</div><div class="vmeta">${v.meta || ''}</div></span>` +
|
|
24
28
|
`<span class="vbadge ${v.badge === 'live' ? 'live' : 'arch'}">${v.badge || 'archive'}</span></div>`).join('');
|
|
25
|
-
return `<div class="vsw" data-
|
|
29
|
+
return `<div class="vsw" data-dropdown><button type="button" class="vsw__btn" data-dropdown-trigger aria-haspopup="listbox" aria-expanded="false" aria-label="Version — ${esc(cur)}">` +
|
|
26
30
|
`<span class="lbl">version:</span><span class="cur">${cur}</span><span class="car"></span></button>` +
|
|
27
|
-
`<div class="vsw__menu" role="listbox" aria-label="Version">${opts}</div></div>`;
|
|
31
|
+
`<div class="vsw__menu" data-dropdown-panel role="listbox" aria-label="Version">${opts}</div></div>`;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
// `nav` ([id, icon, label, href?, target?][]) mirrors the account sidebar so the
|
|
@@ -33,14 +37,15 @@ export function accountMenu({ name = 'Ada Lovelace', email = 'ada@apliteni.com',
|
|
|
33
37
|
const ini = (email.split('@')[0].split(/[._-]+/).filter(Boolean).map((w) => w[0]).slice(0, 2).join('') || '?').toUpperCase();
|
|
34
38
|
const items = nav && nav.length ? nav : [['prefs', 'gear', 'Preferences'], ['access', 'key', 'Access & agents']];
|
|
35
39
|
const it = ([id, ic, label, href, target]) =>
|
|
36
|
-
`<a href="${href || '#' + id}"${target ? ` target="${target}"` : ''}${active === id ? ' class="cur"' : ''} role="menuitem">${icon(ic)}${label}</a>`;
|
|
40
|
+
`<a href="${href || '#' + id}"${target ? ` target="${target}"` : ''} data-dd-item tabindex="-1"${active === id ? ' class="cur"' : ''} role="menuitem">${icon(ic)}${label}</a>`;
|
|
37
41
|
// `on` so the menu is visible in Storybook / standalone use (no /auth/me gate).
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
`<
|
|
42
|
+
// Consumes the shared dropdown wiring via the generic [data-dropdown] hooks.
|
|
43
|
+
return `<div class="acct on" data-dropdown>` +
|
|
44
|
+
`<button class="avatar" data-dropdown-trigger aria-haspopup="menu" aria-expanded="false" aria-label="Account">${ini}</button>` +
|
|
45
|
+
`<div class="amenu" data-dropdown-panel role="menu">` +
|
|
41
46
|
`<div class="ahead"><span class="avatar">${ini}</span><span class="aw"><span class="anm">${name}</span><span class="aem" title="${email}">${email}</span></span></div>` +
|
|
42
47
|
items.map(it).join('') +
|
|
43
|
-
`<div class="asep"></div><a class="aout" href="#logout" role="menuitem">${icon('logout')}Sign out</a>` +
|
|
48
|
+
`<div class="asep"></div><a class="aout" href="#logout" data-dd-item tabindex="-1" role="menuitem">${icon('logout')}Sign out</a>` +
|
|
44
49
|
`</div></div>`;
|
|
45
50
|
}
|
|
46
51
|
|
|
@@ -109,13 +114,9 @@ export function wireTopbar(root = document) {
|
|
|
109
114
|
b.setAttribute('aria-selected', 'true');
|
|
110
115
|
});
|
|
111
116
|
});
|
|
112
|
-
// Version switcher + account menu (open/close
|
|
113
|
-
|
|
114
|
-
root
|
|
115
|
-
root.querySelectorAll('[data-acct]').forEach((ac) => { const b = ac.querySelector('[data-acct-btn]'); b?.addEventListener('click', toggleOpen(ac, b)); });
|
|
116
|
-
document.addEventListener('click', () => {
|
|
117
|
-
root.querySelectorAll('[data-vsw].open,[data-acct].open').forEach((el) => el.classList.remove('open'));
|
|
118
|
-
});
|
|
117
|
+
// Version switcher + account menu — the shared dropdown wiring (open/close +
|
|
118
|
+
// click-outside + Esc + keyboard nav). One implementation for the whole kit.
|
|
119
|
+
wireDropdown(root);
|
|
119
120
|
// Accent pickers
|
|
120
121
|
root.querySelectorAll('[data-accent-pick]').forEach((chip) => {
|
|
121
122
|
chip.addEventListener('click', () => {
|
package/src/index.css
CHANGED
|
@@ -3,18 +3,26 @@
|
|
|
3
3
|
* Import once (`import 'apliteni-ui/css'`) or cherry-pick from src/styles/*.
|
|
4
4
|
* Requires the Poppins font (loaded by the host page or Storybook preview).
|
|
5
5
|
* ========================================================================== */
|
|
6
|
+
@import "./tokens/brand.generated.css";
|
|
6
7
|
@import "./tokens/tokens.css";
|
|
7
8
|
@import "./tokens/accents.css";
|
|
8
9
|
@import "./styles/base.css";
|
|
10
|
+
@import "./styles/motion.css";
|
|
9
11
|
@import "./styles/aurora.css";
|
|
10
12
|
@import "./styles/button.css";
|
|
11
13
|
@import "./styles/card.css";
|
|
12
14
|
@import "./styles/badge.css";
|
|
13
15
|
@import "./styles/segmented.css";
|
|
14
16
|
@import "./styles/input.css";
|
|
17
|
+
@import "./styles/dropdown.css";
|
|
18
|
+
@import "./styles/nav.css";
|
|
19
|
+
@import "./styles/drawer.css";
|
|
15
20
|
@import "./styles/table.css";
|
|
21
|
+
@import "./styles/empty.css";
|
|
16
22
|
@import "./styles/callout.css";
|
|
17
23
|
@import "./styles/code.css";
|
|
18
24
|
@import "./styles/topbar.css";
|
|
25
|
+
@import "./styles/footer.css";
|
|
19
26
|
@import "./styles/layout.css";
|
|
20
27
|
@import "./styles/feedback.css";
|
|
28
|
+
@import "./styles/success.css";
|
package/src/index.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
// apliteni-ui — public entry.
|
|
2
2
|
// Styles ship separately: `import 'apliteni-ui/css'`.
|
|
3
3
|
export * from './components/index.js';
|
|
4
|
+
export * from './components/dropdown.js';
|
|
5
|
+
export * from './components/nav.js';
|
|
6
|
+
export * from './components/drawer.js';
|
|
4
7
|
export * from './components/topbar.js';
|
|
5
8
|
export * from './components/shell.js';
|
|
6
9
|
export * from './components/feedback.js';
|
|
10
|
+
export * from './components/toasts.js';
|
|
7
11
|
export * from './assets/icons.js';
|
|
8
12
|
export * from './assets/brand.js';
|
|
13
|
+
export * from './motion.js';
|
package/src/inline.js
CHANGED
|
@@ -11,9 +11,13 @@ import { readFileSync } from 'node:fs';
|
|
|
11
11
|
|
|
12
12
|
const read = (rel) => readFileSync(new URL(`./${rel}`, import.meta.url), 'utf8');
|
|
13
13
|
|
|
14
|
-
// Tokens =
|
|
15
|
-
// every `:root{ --… }`
|
|
16
|
-
|
|
14
|
+
// Tokens = brand primitives (synced from design-system) + base scale +
|
|
15
|
+
// dark/light + accent sub-themes. The single source for every `:root{ --… }`
|
|
16
|
+
// definition. brand.generated.css goes first so the cascade is right.
|
|
17
|
+
export const tokensCss =
|
|
18
|
+
read('tokens/brand.generated.css') + '\n' +
|
|
19
|
+
read('tokens/tokens.css') + '\n' +
|
|
20
|
+
read('tokens/accents.css');
|
|
17
21
|
|
|
18
22
|
// Base reset, ambient glow, focus ring, default icon sizing.
|
|
19
23
|
export const baseCss = read('styles/base.css');
|
|
@@ -24,34 +28,46 @@ export const topbarCss = read('styles/topbar.css');
|
|
|
24
28
|
// Individual component stylesheets, addressable by name.
|
|
25
29
|
export const styles = {
|
|
26
30
|
base: baseCss,
|
|
31
|
+
motion: read('styles/motion.css'),
|
|
27
32
|
aurora: read('styles/aurora.css'),
|
|
28
33
|
button: read('styles/button.css'),
|
|
29
34
|
card: read('styles/card.css'),
|
|
30
35
|
badge: read('styles/badge.css'),
|
|
31
36
|
segmented: read('styles/segmented.css'),
|
|
32
37
|
input: read('styles/input.css'),
|
|
38
|
+
dropdown: read('styles/dropdown.css'),
|
|
39
|
+
nav: read('styles/nav.css'),
|
|
40
|
+
drawer: read('styles/drawer.css'),
|
|
33
41
|
table: read('styles/table.css'),
|
|
34
42
|
callout: read('styles/callout.css'),
|
|
35
43
|
code: read('styles/code.css'),
|
|
36
44
|
topbar: topbarCss,
|
|
45
|
+
footer: read('styles/footer.css'),
|
|
37
46
|
layout: read('styles/layout.css'),
|
|
38
47
|
feedback: read('styles/feedback.css'),
|
|
48
|
+
success: read('styles/success.css'),
|
|
39
49
|
};
|
|
40
50
|
|
|
41
51
|
// Everything, in the same order as index.css. `tokensCss` first so cascade is right.
|
|
42
52
|
export const cssText = [
|
|
43
53
|
tokensCss,
|
|
44
54
|
styles.base,
|
|
55
|
+
styles.motion,
|
|
45
56
|
styles.aurora,
|
|
46
57
|
styles.button,
|
|
47
58
|
styles.card,
|
|
48
59
|
styles.badge,
|
|
49
60
|
styles.segmented,
|
|
50
61
|
styles.input,
|
|
62
|
+
styles.dropdown,
|
|
63
|
+
styles.nav,
|
|
64
|
+
styles.drawer,
|
|
51
65
|
styles.table,
|
|
52
66
|
styles.callout,
|
|
53
67
|
styles.code,
|
|
54
68
|
styles.topbar,
|
|
69
|
+
styles.footer,
|
|
55
70
|
styles.layout,
|
|
56
71
|
styles.feedback,
|
|
72
|
+
styles.success,
|
|
57
73
|
].join('\n');
|
package/src/motion.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// apliteni-ui — motion hook.
|
|
2
|
+
//
|
|
3
|
+
// The one small, optional piece of JS behind the CSS motion library: a
|
|
4
|
+
// scroll-reveal activator plus a replay helper. Everything else is pure CSS
|
|
5
|
+
// (src/styles/motion.css). Framework-agnostic, no dependencies, guarded so it
|
|
6
|
+
// no-ops cleanly under `node --test` / SSR (no window, no IntersectionObserver).
|
|
7
|
+
//
|
|
8
|
+
// import { initReveal } from 'apliteni-ui/motion'
|
|
9
|
+
// initReveal(); // wire every [data-reveal] on the page
|
|
10
|
+
// initReveal(myContainer); // scope to a subtree
|
|
11
|
+
//
|
|
12
|
+
// Stagger a group by giving siblings --reveal-i: 0, 1, 2, … (see motion.css).
|
|
13
|
+
|
|
14
|
+
/** True only when the user asked the OS to reduce motion. Safe off-DOM. */
|
|
15
|
+
export function prefersReducedMotion() {
|
|
16
|
+
return (
|
|
17
|
+
typeof matchMedia === 'function' &&
|
|
18
|
+
matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Stagger delay for the Nth item given a per-step delay. Pure + testable. */
|
|
23
|
+
export function staggerDelay(index, stepMs = 120) {
|
|
24
|
+
return Math.max(0, index) * stepMs;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Reveal every [data-reveal] under `root` as it scrolls into view.
|
|
29
|
+
* - Marks <html class="js-reveal"> so the CSS hides them only when JS is live.
|
|
30
|
+
* - Reduced-motion or no IntersectionObserver → reveal everything at once.
|
|
31
|
+
* Returns the observer (or undefined when there's nothing to observe).
|
|
32
|
+
*/
|
|
33
|
+
export function initReveal(root) {
|
|
34
|
+
if (typeof document === 'undefined') return undefined;
|
|
35
|
+
const scope = root || document;
|
|
36
|
+
const els = scope.querySelectorAll('[data-reveal]');
|
|
37
|
+
if (!els.length) return undefined;
|
|
38
|
+
|
|
39
|
+
document.documentElement.classList.add('js-reveal');
|
|
40
|
+
const show = (el) => el.classList.add('is-revealed');
|
|
41
|
+
|
|
42
|
+
if (prefersReducedMotion() || typeof IntersectionObserver === 'undefined') {
|
|
43
|
+
els.forEach(show);
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const io = new IntersectionObserver(
|
|
48
|
+
(entries, obs) => {
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (entry.isIntersecting) {
|
|
51
|
+
show(entry.target);
|
|
52
|
+
obs.unobserve(entry.target);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{ rootMargin: '0px 0px -10% 0px', threshold: 0.05 },
|
|
57
|
+
);
|
|
58
|
+
els.forEach((el) => io.observe(el));
|
|
59
|
+
return io;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Restart the CSS animation on an element (for a "replay" control). */
|
|
63
|
+
export function replay(el) {
|
|
64
|
+
if (!el || !el.style) return;
|
|
65
|
+
el.style.animation = 'none';
|
|
66
|
+
void el.offsetWidth; // force reflow so the next frame re-triggers
|
|
67
|
+
el.style.animation = '';
|
|
68
|
+
}
|
package/src/styles/button.css
CHANGED
|
@@ -89,8 +89,12 @@
|
|
|
89
89
|
pointer-events: none;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
/* Busy: keep the label, run
|
|
93
|
-
button is disabled (not clickable). Pass busy + the .ui-btn__bars markup.
|
|
92
|
+
/* Busy: keep the label, run an indeterminate accent shimmer along the base, and
|
|
93
|
+
the button is disabled (not clickable). Pass busy + the .ui-btn__bars markup.
|
|
94
|
+
The track is inset into a rounded pill so it sits INSIDE the button — not
|
|
95
|
+
flush against the clipped corners — and tints from the live --accent so it
|
|
96
|
+
reads as an intentional progress element in every theme (and mutes uniformly
|
|
97
|
+
with the button in the disabled state). */
|
|
94
98
|
.ui-btn[aria-busy="true"] {
|
|
95
99
|
pointer-events: none;
|
|
96
100
|
position: relative;
|
|
@@ -98,21 +102,46 @@
|
|
|
98
102
|
}
|
|
99
103
|
.ui-btn__bars {
|
|
100
104
|
position: absolute;
|
|
101
|
-
left:
|
|
105
|
+
left: 10px; right: 10px; bottom: 5px;
|
|
102
106
|
height: 3px;
|
|
103
|
-
|
|
107
|
+
border-radius: 999px;
|
|
108
|
+
background: color-mix(in srgb, var(--accent) 22%, transparent);
|
|
104
109
|
overflow: hidden;
|
|
110
|
+
/* Clean entrance: grow + fade in on state change, once. */
|
|
111
|
+
transform-origin: center;
|
|
112
|
+
animation: ui-bars-in 180ms var(--ease) both;
|
|
105
113
|
}
|
|
114
|
+
.ui-btn--sm .ui-btn__bars { left: 7px; right: 7px; bottom: 4px; }
|
|
115
|
+
.ui-btn--lg .ui-btn__bars { left: 14px; right: 14px; bottom: 7px; }
|
|
106
116
|
.ui-btn__bars i {
|
|
107
117
|
position: absolute;
|
|
108
118
|
height: 100%;
|
|
109
|
-
border-radius:
|
|
110
|
-
background: linear-gradient(90deg,
|
|
119
|
+
border-radius: 999px;
|
|
120
|
+
background: linear-gradient(90deg,
|
|
121
|
+
color-mix(in srgb, var(--accent) 30%, transparent), var(--accent));
|
|
122
|
+
}
|
|
123
|
+
/* On the filled primary the surface IS the accent, so shimmer in the contrast
|
|
124
|
+
colour instead — otherwise the bars vanish into the button. */
|
|
125
|
+
.ui-btn--primary .ui-btn__bars {
|
|
126
|
+
background: color-mix(in srgb, var(--accent-contrast) 26%, transparent);
|
|
127
|
+
}
|
|
128
|
+
.ui-btn--primary .ui-btn__bars i {
|
|
129
|
+
background: linear-gradient(90deg,
|
|
130
|
+
color-mix(in srgb, var(--accent-contrast) 35%, transparent),
|
|
131
|
+
var(--accent-contrast));
|
|
132
|
+
}
|
|
133
|
+
.ui-btn__bars i:nth-child(1) { width: 38%; animation: ui-bars1 2s cubic-bezier(0.65, 0.05, 0.35, 1) infinite; }
|
|
134
|
+
.ui-btn__bars i:nth-child(2) { width: 22%; animation: ui-bars2 2s cubic-bezier(0.65, 0.05, 0.35, 1) 0.8s infinite; }
|
|
135
|
+
@keyframes ui-bars1 { 0% { left: -45%; } 100% { left: 110%; } }
|
|
136
|
+
@keyframes ui-bars2 { 0% { left: -28%; } 100% { left: 120%; } }
|
|
137
|
+
@keyframes ui-bars-in { from { opacity: 0; transform: scaleX(0.72); } to { opacity: 1; transform: scaleX(1); } }
|
|
138
|
+
/* Reduced motion: no entrance, no sweep — just a static, intentional accent
|
|
139
|
+
fill so the busy state still reads clearly. */
|
|
140
|
+
@media (prefers-reduced-motion: reduce) {
|
|
141
|
+
.ui-btn__bars { animation: none; opacity: 1; transform: none; }
|
|
142
|
+
.ui-btn__bars i { animation: none; }
|
|
143
|
+
.ui-btn__bars i:nth-child(1) { left: 0; width: 100%; }
|
|
144
|
+
.ui-btn__bars i:nth-child(2) { display: none; }
|
|
111
145
|
}
|
|
112
|
-
.ui-btn__bars i:nth-child(1) { width: 35%; animation: ui-bars1 2s cubic-bezier(0.65, 0.05, 0.35, 1) infinite; }
|
|
113
|
-
.ui-btn__bars i:nth-child(2) { width: 20%; animation: ui-bars2 2s cubic-bezier(0.65, 0.05, 0.35, 1) 0.8s infinite; }
|
|
114
|
-
@keyframes ui-bars1 { 0% { left: -40%; } 100% { left: 110%; } }
|
|
115
|
-
@keyframes ui-bars2 { 0% { left: -25%; } 100% { left: 120%; } }
|
|
116
|
-
@media (prefers-reduced-motion: reduce) { .ui-btn__bars i { animation: none; } .ui-btn__bars i:nth-child(1) { left: 0; } }
|
|
117
146
|
|
|
118
147
|
@keyframes ui-spin { to { transform: rotate(360deg); } }
|
package/src/styles/callout.css
CHANGED
|
@@ -26,28 +26,95 @@
|
|
|
26
26
|
.ui-callout--danger { background: var(--glow-pink); }
|
|
27
27
|
.ui-callout--danger .ui-callout__icon { color: var(--pink); }
|
|
28
28
|
|
|
29
|
-
/*
|
|
29
|
+
/* ----------------------------------------------------------------------------
|
|
30
|
+
* Toast — a floating, dismissible notification.
|
|
31
|
+
* A status (colour) × style (surface) matrix, entirely token-driven:
|
|
32
|
+
* status success · danger · warn · info · neutral → sets --toast-accent/-glow/-on
|
|
33
|
+
* style soft (tinted) · solid (filled) · outline (bordered)
|
|
34
|
+
* -------------------------------------------------------------------------- */
|
|
30
35
|
.ui-toast {
|
|
36
|
+
position: relative;
|
|
31
37
|
display: flex;
|
|
32
|
-
align-items:
|
|
38
|
+
align-items: flex-start;
|
|
33
39
|
gap: 12px;
|
|
34
|
-
padding: 13px
|
|
40
|
+
padding: 13px 15px;
|
|
35
41
|
border-radius: var(--radius-md);
|
|
36
|
-
background: var(--bg-elevated);
|
|
37
|
-
box-shadow: var(--shadow-lg);
|
|
38
42
|
color: var(--text);
|
|
39
43
|
font-size: var(--text-base);
|
|
40
|
-
|
|
44
|
+
width: 100%;
|
|
45
|
+
max-width: 400px;
|
|
46
|
+
overflow: hidden; /* clips the timer bar + left marker to the rounded corners */
|
|
47
|
+
animation: ui-toast-in 0.28s cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
41
48
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
.ui-toast--
|
|
46
|
-
.ui-
|
|
49
|
+
|
|
50
|
+
/* status → the paint tokens every style below consumes (--toast-on = the ink
|
|
51
|
+
that reads on the accent when it becomes a fill) */
|
|
52
|
+
.ui-toast--success { --toast-accent: var(--green); --toast-glow: var(--glow-green); --toast-on: #0c0c0c; }
|
|
53
|
+
.ui-toast--danger { --toast-accent: var(--pink); --toast-glow: var(--glow-pink); --toast-on: #ffffff; }
|
|
54
|
+
.ui-toast--warn { --toast-accent: var(--amber); --toast-glow: color-mix(in srgb, var(--amber) 14%, transparent); --toast-on: #0c0c0c; }
|
|
55
|
+
.ui-toast--info { --toast-accent: var(--cyan); --toast-glow: var(--glow-cyan); --toast-on: #0c0c0c; }
|
|
56
|
+
.ui-toast--neutral { --toast-accent: var(--muted); --toast-glow: var(--surface-2); --toast-on: var(--strong); }
|
|
57
|
+
|
|
58
|
+
/* left status marker (soft + outline; solid is already a full fill) */
|
|
59
|
+
.ui-toast--soft::before,
|
|
60
|
+
.ui-toast--outline::before {
|
|
61
|
+
content: ''; position: absolute; left: 0; top: 0; bottom: 0;
|
|
62
|
+
width: 3px; background: var(--toast-accent);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.ui-toast__icon { width: 22px; height: 22px; flex: none; margin-top: 1px; display: grid; place-items: center; border-radius: 50%; background: var(--toast-accent); color: var(--toast-on); }
|
|
66
|
+
.ui-toast__icon svg { width: 13px; height: 13px; stroke: currentColor; fill: none; stroke-width: 2; }
|
|
67
|
+
.ui-toast__body { flex: 1; min-width: 0; }
|
|
47
68
|
.ui-toast__title { font-weight: var(--weight-medium); color: var(--strong); }
|
|
48
|
-
.ui-
|
|
69
|
+
.ui-toast__text { color: var(--dim); }
|
|
70
|
+
|
|
71
|
+
/* trailing action ("Undo" / "Retry") */
|
|
72
|
+
.ui-toast__action { flex: none; align-self: center; background: none; border: 0; padding: 4px 9px; border-radius: var(--radius-xs); font: inherit; font-weight: var(--weight-medium); color: var(--toast-accent); cursor: pointer; }
|
|
73
|
+
.ui-toast__action:hover { background: color-mix(in srgb, var(--toast-accent) 14%, transparent); }
|
|
74
|
+
|
|
75
|
+
.ui-toast__close { flex: none; align-self: flex-start; background: none; border: 0; color: var(--muted); cursor: pointer; padding: 2px; display: grid; place-items: center; }
|
|
76
|
+
.ui-toast__close svg { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width: 1.8; }
|
|
49
77
|
.ui-toast__close:hover { color: var(--text); }
|
|
50
78
|
|
|
79
|
+
/* auto-dismiss progress bar — .is-running is added by wireToastStack() */
|
|
80
|
+
.ui-toast__timer { position: absolute; left: 0; bottom: 0; height: 3px; width: 100%; background: var(--toast-accent); opacity: 0.85; transform-origin: left; }
|
|
81
|
+
.ui-toast__timer.is-running { animation: ui-toast-timer var(--toast-dur, 5s) linear forwards; }
|
|
82
|
+
|
|
83
|
+
/* style: soft — tinted surface */
|
|
84
|
+
.ui-toast--soft { background: var(--toast-glow); box-shadow: var(--shadow-sm); }
|
|
85
|
+
|
|
86
|
+
/* style: solid — filled accent, ink flips to --toast-on */
|
|
87
|
+
.ui-toast--solid { background: var(--toast-accent); color: var(--toast-on); box-shadow: var(--shadow-lg); }
|
|
88
|
+
.ui-toast--solid .ui-toast__title,
|
|
89
|
+
.ui-toast--solid .ui-toast__text { color: var(--toast-on); }
|
|
90
|
+
.ui-toast--solid .ui-toast__text { opacity: 0.82; }
|
|
91
|
+
.ui-toast--solid .ui-toast__icon { background: color-mix(in srgb, var(--toast-on) 22%, transparent); color: var(--toast-on); }
|
|
92
|
+
.ui-toast--solid .ui-toast__action { color: var(--toast-on); }
|
|
93
|
+
.ui-toast--solid .ui-toast__action:hover { background: color-mix(in srgb, var(--toast-on) 18%, transparent); }
|
|
94
|
+
.ui-toast--solid .ui-toast__close { color: color-mix(in srgb, var(--toast-on) 68%, transparent); }
|
|
95
|
+
.ui-toast--solid .ui-toast__close:hover { color: var(--toast-on); }
|
|
96
|
+
.ui-toast--solid .ui-toast__timer { background: color-mix(in srgb, var(--toast-on) 55%, transparent); }
|
|
97
|
+
|
|
98
|
+
/* style: outline — elevated card with a status-tinted border */
|
|
99
|
+
.ui-toast--outline { background: var(--bg-elevated); box-shadow: var(--shadow-md); border: 1px solid color-mix(in srgb, var(--toast-accent) 40%, transparent); }
|
|
100
|
+
|
|
101
|
+
/* compact — single line, no body text */
|
|
102
|
+
.ui-toast--compact { align-items: center; padding-top: 10px; padding-bottom: 10px; }
|
|
103
|
+
.ui-toast--compact .ui-toast__icon { margin-top: 0; }
|
|
104
|
+
|
|
105
|
+
@keyframes ui-toast-in { from { opacity: 0; transform: translateX(18px); } to { opacity: 1; transform: none; } }
|
|
106
|
+
@keyframes ui-toast-out { to { opacity: 0; transform: translateX(24px); } }
|
|
107
|
+
@keyframes ui-toast-timer { from { transform: scaleX(1); } to { transform: scaleX(0); } }
|
|
108
|
+
.ui-toast.is-leaving { animation: ui-toast-out 0.2s ease forwards; }
|
|
109
|
+
|
|
110
|
+
@media (prefers-reduced-motion: reduce) {
|
|
111
|
+
.ui-toast, .ui-toast.is-leaving { animation: none; }
|
|
112
|
+
.ui-toast__timer.is-running { animation: none; }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* stack container for queued toasts */
|
|
116
|
+
.ui-toast-stack { display: flex; flex-direction: column; gap: 12px; width: 400px; max-width: 100%; }
|
|
117
|
+
|
|
51
118
|
/* Success panel — centered confirmation, used after form submit */
|
|
52
119
|
.ui-success {
|
|
53
120
|
border-radius: var(--radius-lg);
|