@apliteni/apliteni-ui 0.2.4
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/LICENSE +6 -0
- package/README.md +159 -0
- package/package.json +49 -0
- package/src/assets/brand.js +18 -0
- package/src/assets/icons.js +36 -0
- package/src/components/feedback.js +171 -0
- package/src/components/index.js +126 -0
- package/src/components/shell.js +46 -0
- package/src/components/topbar.js +133 -0
- package/src/index.css +19 -0
- package/src/index.js +8 -0
- package/src/inline.js +55 -0
- package/src/styles/badge.css +64 -0
- package/src/styles/base.css +127 -0
- package/src/styles/button.css +118 -0
- package/src/styles/callout.css +69 -0
- package/src/styles/card.css +79 -0
- package/src/styles/code.css +72 -0
- package/src/styles/feedback.css +113 -0
- package/src/styles/input.css +155 -0
- package/src/styles/layout.css +164 -0
- package/src/styles/segmented.css +55 -0
- package/src/styles/table.css +51 -0
- package/src/styles/topbar.css +199 -0
- package/src/tokens/accents.css +92 -0
- package/src/tokens/tokens.css +185 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Topbar factory + client behaviours. Composes the canonical strategy topbar
|
|
2
|
+
// (brand, Deck/Text, theme toggle, version switcher, account menu).
|
|
3
|
+
import { brand } from '../assets/brand.js';
|
|
4
|
+
import { icon, sun, moon } from '../assets/icons.js';
|
|
5
|
+
|
|
6
|
+
const THEME_KEY = 'apliteni-strategy-theme';
|
|
7
|
+
|
|
8
|
+
export function themeToggle() {
|
|
9
|
+
return `<button class="toggle" data-theme-toggle aria-label="Toggle theme"><span class="ic" data-theme-icon></span><span data-theme-label>Light</span></button>`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function deckTextSwitch(active = 'deck') {
|
|
13
|
+
return `<div class="dtsw on" role="group" aria-label="View">` +
|
|
14
|
+
`<a${active === 'deck' ? ' class="cur" aria-current="page"' : ''} href="#deck">Deck</a>` +
|
|
15
|
+
`<a${active === 'text' ? ' class="cur" aria-current="page"' : ''} href="#text">Text</a></div>`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function versionSwitcher(versions = [], activeIdx = 0) {
|
|
19
|
+
const cur = versions[activeIdx]?.label || '';
|
|
20
|
+
const opts = versions.map((v, i) =>
|
|
21
|
+
`<div class="vopt" role="option" data-active="${i === activeIdx ? '1' : '0'}">` +
|
|
22
|
+
`<span><div class="vname">${v.label}</div><div class="vmeta">${v.meta || ''}</div></span>` +
|
|
23
|
+
`<span class="vbadge ${v.badge === 'live' ? 'live' : 'arch'}">${v.badge || 'archive'}</span></div>`).join('');
|
|
24
|
+
return `<div class="vsw" data-vsw><button class="vsw__btn" aria-haspopup="listbox" aria-expanded="false">` +
|
|
25
|
+
`<span class="lbl">version:</span><span class="cur">${cur}</span><span class="car"></span></button>` +
|
|
26
|
+
`<div class="vsw__menu" role="listbox">${opts}</div></div>`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// `nav` ([id, icon, label, href?, target?][]) mirrors the account sidebar so the
|
|
30
|
+
// dropdown and the sidebar stay in sync; falls back to the default two items.
|
|
31
|
+
export function accountMenu({ name = 'Ada Lovelace', email = 'ada@apliteni.com', active = 'prefs', nav } = {}) {
|
|
32
|
+
const ini = (email.split('@')[0].split(/[._-]+/).filter(Boolean).map((w) => w[0]).slice(0, 2).join('') || '?').toUpperCase();
|
|
33
|
+
const items = nav && nav.length ? nav : [['prefs', 'gear', 'Preferences'], ['access', 'key', 'Access & agents']];
|
|
34
|
+
const it = ([id, ic, label, href, target]) =>
|
|
35
|
+
`<a href="${href || '#' + id}"${target ? ` target="${target}"` : ''}${active === id ? ' class="cur"' : ''} role="menuitem">${icon(ic)}${label}</a>`;
|
|
36
|
+
// `on` so the menu is visible in Storybook / standalone use (no /auth/me gate).
|
|
37
|
+
return `<div class="acct on" data-acct>` +
|
|
38
|
+
`<button class="avatar" data-acct-btn aria-haspopup="menu" aria-expanded="false" aria-label="Account">${ini}</button>` +
|
|
39
|
+
`<div class="amenu" role="menu">` +
|
|
40
|
+
`<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>` +
|
|
41
|
+
items.map(it).join('') +
|
|
42
|
+
`<div class="asep"></div><a class="aout" href="#logout" role="menuitem">${icon('logout')}Sign out</a>` +
|
|
43
|
+
`</div></div>`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Full topbar. Pass which pieces to include.
|
|
47
|
+
export function topbar({
|
|
48
|
+
word = 'Strategy', view = 'deck', versions, versionIdx = 0,
|
|
49
|
+
account, theme = true, showSwitch = true,
|
|
50
|
+
} = {}) {
|
|
51
|
+
return `<header class="topbar"><div class="topbar__in">` +
|
|
52
|
+
brand({ word }) +
|
|
53
|
+
(showSwitch ? deckTextSwitch(view) : '') +
|
|
54
|
+
`<span class="spacer"></span>` +
|
|
55
|
+
(versions ? versionSwitcher(versions, versionIdx) : '') +
|
|
56
|
+
(theme ? themeToggle() : '') +
|
|
57
|
+
(account ? accountMenu(account) : '') +
|
|
58
|
+
`</div></header>`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---- Behaviours (call once after markup mounts) --------------------------
|
|
62
|
+
export function applyTheme(t, root = document.documentElement) {
|
|
63
|
+
root.setAttribute('data-theme', t);
|
|
64
|
+
root.querySelectorAll('[data-theme-toggle]').forEach((btn) => {
|
|
65
|
+
const ic = btn.querySelector('[data-theme-icon]');
|
|
66
|
+
const lbl = btn.querySelector('[data-theme-label]');
|
|
67
|
+
if (ic) ic.innerHTML = t === 'dark' ? sun : moon;
|
|
68
|
+
if (lbl) lbl.textContent = t === 'dark' ? 'Light' : 'Dark';
|
|
69
|
+
});
|
|
70
|
+
try { localStorage.setItem(THEME_KEY, t); } catch (e) { /* no-op */ }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const ACCENT_KEY = 'apliteni-strategy-accent';
|
|
74
|
+
export const ACCENTS = ['default', 'phoenix', 'ocean', 'emerald'];
|
|
75
|
+
|
|
76
|
+
export function applyAccent(name, root = document.documentElement) {
|
|
77
|
+
if (!name || name === 'default') root.removeAttribute('data-accent');
|
|
78
|
+
else root.setAttribute('data-accent', name);
|
|
79
|
+
try { localStorage.setItem(ACCENT_KEY, name || 'default'); } catch (e) { /* no-op */ }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function wireTopbar(root = document) {
|
|
83
|
+
// Theme toggle
|
|
84
|
+
root.querySelectorAll('[data-theme-toggle]').forEach((btn) => {
|
|
85
|
+
const html = document.documentElement;
|
|
86
|
+
const cur = html.getAttribute('data-theme') || 'dark';
|
|
87
|
+
applyTheme(cur, html);
|
|
88
|
+
btn.addEventListener('click', () => applyTheme(html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark', html));
|
|
89
|
+
});
|
|
90
|
+
// Deck/Text view switch (.dtsw) — switch the active pill on click
|
|
91
|
+
root.querySelectorAll('.dtsw').forEach((sw) => {
|
|
92
|
+
sw.addEventListener('click', (e) => {
|
|
93
|
+
const a = e.target.closest('a');
|
|
94
|
+
if (!a || !sw.contains(a)) return;
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
sw.querySelectorAll('a').forEach((x) => { x.classList.remove('cur'); x.removeAttribute('aria-current'); });
|
|
97
|
+
a.classList.add('cur');
|
|
98
|
+
a.setAttribute('aria-current', 'page');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
// Segmented controls (.ui-seg) — visual toggle
|
|
102
|
+
root.querySelectorAll('.ui-seg').forEach((seg) => {
|
|
103
|
+
seg.addEventListener('click', (e) => {
|
|
104
|
+
const b = e.target.closest('button');
|
|
105
|
+
if (!b || !seg.contains(b)) return;
|
|
106
|
+
seg.querySelectorAll('button').forEach((x) => { x.classList.remove('is-active'); x.setAttribute('aria-selected', 'false'); });
|
|
107
|
+
b.classList.add('is-active');
|
|
108
|
+
b.setAttribute('aria-selected', 'true');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
// Version switcher + account menu (open/close)
|
|
112
|
+
const toggleOpen = (el, btn) => (e) => { e.stopPropagation(); const o = el.classList.toggle('open'); btn?.setAttribute('aria-expanded', o ? 'true' : 'false'); };
|
|
113
|
+
root.querySelectorAll('[data-vsw]').forEach((vsw) => { const b = vsw.querySelector('.vsw__btn'); b?.addEventListener('click', toggleOpen(vsw, b)); });
|
|
114
|
+
root.querySelectorAll('[data-acct]').forEach((ac) => { const b = ac.querySelector('[data-acct-btn]'); b?.addEventListener('click', toggleOpen(ac, b)); });
|
|
115
|
+
document.addEventListener('click', () => {
|
|
116
|
+
root.querySelectorAll('[data-vsw].open,[data-acct].open').forEach((el) => el.classList.remove('open'));
|
|
117
|
+
});
|
|
118
|
+
// Accent pickers
|
|
119
|
+
root.querySelectorAll('[data-accent-pick]').forEach((chip) => {
|
|
120
|
+
chip.addEventListener('click', () => {
|
|
121
|
+
applyAccent(chip.getAttribute('data-accent-pick'), document.documentElement);
|
|
122
|
+
const group = chip.closest('[data-accent-group]') || root;
|
|
123
|
+
group.querySelectorAll('[data-accent-pick]').forEach((c) => c.classList.toggle('is-active', c === chip));
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
// Copy buttons
|
|
127
|
+
root.querySelectorAll('.ui-snippet__copy').forEach((btn) => {
|
|
128
|
+
btn.addEventListener('click', () => {
|
|
129
|
+
const pre = btn.closest('.ui-snippet')?.querySelector('pre');
|
|
130
|
+
if (pre) { navigator.clipboard?.writeText(pre.innerText); btn.innerHTML = '✓ Copied'; setTimeout(() => { btn.innerHTML = btn.dataset.orig || 'Copy'; }, 1400); }
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
package/src/index.css
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* apliteni-ui — full stylesheet.
|
|
3
|
+
* Import once (`import 'apliteni-ui/css'`) or cherry-pick from src/styles/*.
|
|
4
|
+
* Requires the Poppins font (loaded by the host page or Storybook preview).
|
|
5
|
+
* ========================================================================== */
|
|
6
|
+
@import "./tokens/tokens.css";
|
|
7
|
+
@import "./tokens/accents.css";
|
|
8
|
+
@import "./styles/base.css";
|
|
9
|
+
@import "./styles/button.css";
|
|
10
|
+
@import "./styles/card.css";
|
|
11
|
+
@import "./styles/badge.css";
|
|
12
|
+
@import "./styles/segmented.css";
|
|
13
|
+
@import "./styles/input.css";
|
|
14
|
+
@import "./styles/table.css";
|
|
15
|
+
@import "./styles/callout.css";
|
|
16
|
+
@import "./styles/code.css";
|
|
17
|
+
@import "./styles/topbar.css";
|
|
18
|
+
@import "./styles/layout.css";
|
|
19
|
+
@import "./styles/feedback.css";
|
package/src/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// apliteni-ui — public entry.
|
|
2
|
+
// Styles ship separately: `import 'apliteni-ui/css'`.
|
|
3
|
+
export * from './components/index.js';
|
|
4
|
+
export * from './components/topbar.js';
|
|
5
|
+
export * from './components/shell.js';
|
|
6
|
+
export * from './components/feedback.js';
|
|
7
|
+
export * from './assets/icons.js';
|
|
8
|
+
export * from './assets/brand.js';
|
package/src/inline.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Inline CSS for server-render consumers (e.g. the strategy portal, which
|
|
2
|
+
// inlines stylesheets into the HTML it serves rather than linking them).
|
|
3
|
+
//
|
|
4
|
+
// Node reads the package's own .css files and returns them as strings. Import
|
|
5
|
+
// only what a page needs:
|
|
6
|
+
//
|
|
7
|
+
// import { tokensCss, topbarCss, cssText } from '@apliteni/apliteni-ui/inline'
|
|
8
|
+
//
|
|
9
|
+
// Browser/bundler builds should import the .css directly ('@apliteni/apliteni-ui/css').
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
|
|
12
|
+
const read = (rel) => readFileSync(new URL(`./${rel}`, import.meta.url), 'utf8');
|
|
13
|
+
|
|
14
|
+
// Tokens = base scale + dark/light + accent sub-themes. The single source for
|
|
15
|
+
// every `:root{ --… }` definition. Replaces hand-inlined token blocks.
|
|
16
|
+
export const tokensCss = read('tokens/tokens.css') + '\n' + read('tokens/accents.css');
|
|
17
|
+
|
|
18
|
+
// Base reset, ambient glow, focus ring, default icon sizing.
|
|
19
|
+
export const baseCss = read('styles/base.css');
|
|
20
|
+
|
|
21
|
+
// Canonical topbar (same class names the portal already uses).
|
|
22
|
+
export const topbarCss = read('styles/topbar.css');
|
|
23
|
+
|
|
24
|
+
// Individual component stylesheets, addressable by name.
|
|
25
|
+
export const styles = {
|
|
26
|
+
base: baseCss,
|
|
27
|
+
button: read('styles/button.css'),
|
|
28
|
+
card: read('styles/card.css'),
|
|
29
|
+
badge: read('styles/badge.css'),
|
|
30
|
+
segmented: read('styles/segmented.css'),
|
|
31
|
+
input: read('styles/input.css'),
|
|
32
|
+
table: read('styles/table.css'),
|
|
33
|
+
callout: read('styles/callout.css'),
|
|
34
|
+
code: read('styles/code.css'),
|
|
35
|
+
topbar: topbarCss,
|
|
36
|
+
layout: read('styles/layout.css'),
|
|
37
|
+
feedback: read('styles/feedback.css'),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Everything, in the same order as index.css. `tokensCss` first so cascade is right.
|
|
41
|
+
export const cssText = [
|
|
42
|
+
tokensCss,
|
|
43
|
+
styles.base,
|
|
44
|
+
styles.button,
|
|
45
|
+
styles.card,
|
|
46
|
+
styles.badge,
|
|
47
|
+
styles.segmented,
|
|
48
|
+
styles.input,
|
|
49
|
+
styles.table,
|
|
50
|
+
styles.callout,
|
|
51
|
+
styles.code,
|
|
52
|
+
styles.topbar,
|
|
53
|
+
styles.layout,
|
|
54
|
+
styles.feedback,
|
|
55
|
+
].join('\n');
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* Badge / Pill / Tag
|
|
3
|
+
* .ui-badge — uppercase status chip (live / soon / archive / neutral)
|
|
4
|
+
* .ui-tag — soft solid label
|
|
5
|
+
* .ui-dot — leading status dot (pulses when .is-live)
|
|
6
|
+
* ========================================================================== */
|
|
7
|
+
|
|
8
|
+
.ui-badge {
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
gap: 6px;
|
|
12
|
+
font-size: 10px;
|
|
13
|
+
letter-spacing: 0.12em;
|
|
14
|
+
text-transform: uppercase;
|
|
15
|
+
font-weight: var(--weight-bold);
|
|
16
|
+
padding: 3px 9px;
|
|
17
|
+
border-radius: var(--radius-pill);
|
|
18
|
+
color: var(--muted);
|
|
19
|
+
background: var(--surface-2);
|
|
20
|
+
white-space: nowrap;
|
|
21
|
+
}
|
|
22
|
+
.ui-badge--live { color: var(--green); background: var(--glow-green); }
|
|
23
|
+
.ui-badge--soon { color: var(--purple-mid); background: var(--glow-purple); }
|
|
24
|
+
.ui-badge--info { color: var(--cyan); background: var(--glow-cyan); }
|
|
25
|
+
.ui-badge--warn { color: var(--amber); background: color-mix(in srgb, var(--amber) 15%, transparent); }
|
|
26
|
+
.ui-badge--danger { color: var(--pink); background: var(--glow-pink); }
|
|
27
|
+
.ui-badge--archive { color: var(--muted); background: var(--surface); }
|
|
28
|
+
|
|
29
|
+
/* Pill: same shape, sentence case, a touch larger — a metadata chip */
|
|
30
|
+
.ui-pill {
|
|
31
|
+
display: inline-flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: 7px;
|
|
34
|
+
font-size: var(--text-xs);
|
|
35
|
+
letter-spacing: 0.12em;
|
|
36
|
+
text-transform: uppercase;
|
|
37
|
+
color: var(--muted);
|
|
38
|
+
background: var(--surface-2);
|
|
39
|
+
border-radius: var(--radius-pill);
|
|
40
|
+
padding: 4px 11px;
|
|
41
|
+
font-weight: var(--weight-semibold);
|
|
42
|
+
}
|
|
43
|
+
.ui-pill--live { color: var(--green); background: var(--glow-green); }
|
|
44
|
+
.ui-pill--soon { color: var(--purple-mid); background: var(--glow-purple); }
|
|
45
|
+
|
|
46
|
+
/* Status dot */
|
|
47
|
+
.ui-dot {
|
|
48
|
+
width: 8px;
|
|
49
|
+
height: 8px;
|
|
50
|
+
border-radius: 50%;
|
|
51
|
+
background: var(--muted);
|
|
52
|
+
flex: none;
|
|
53
|
+
display: inline-block;
|
|
54
|
+
}
|
|
55
|
+
.ui-dot.is-live {
|
|
56
|
+
background: var(--green);
|
|
57
|
+
box-shadow: 0 0 0 0 var(--glow-green);
|
|
58
|
+
animation: ui-pulse 2s var(--ease) infinite;
|
|
59
|
+
}
|
|
60
|
+
@keyframes ui-pulse {
|
|
61
|
+
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--green) 55%, transparent); }
|
|
62
|
+
70% { box-shadow: 0 0 0 7px transparent; }
|
|
63
|
+
100% { box-shadow: 0 0 0 0 transparent; }
|
|
64
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* base — reset, document defaults, ambient glow, typography primitives
|
|
3
|
+
* ========================================================================== */
|
|
4
|
+
|
|
5
|
+
*,
|
|
6
|
+
*::before,
|
|
7
|
+
*::after {
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
html {
|
|
14
|
+
scroll-behavior: smooth;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
body {
|
|
18
|
+
background: var(--bg);
|
|
19
|
+
color: var(--ink);
|
|
20
|
+
font-family: var(--font-sans);
|
|
21
|
+
font-weight: var(--weight-normal);
|
|
22
|
+
font-size: var(--text-base);
|
|
23
|
+
line-height: var(--leading-normal);
|
|
24
|
+
-webkit-font-smoothing: antialiased;
|
|
25
|
+
text-rendering: optimizeLegibility;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
::selection {
|
|
29
|
+
background: color-mix(in srgb, var(--accent) 30%, transparent);
|
|
30
|
+
color: var(--strong);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
a {
|
|
34
|
+
color: var(--accent);
|
|
35
|
+
text-decoration: none;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Ambient blurred glow — the deck's signature background depth.
|
|
39
|
+
Place <span class="ui-glow ui-glow--purple"> absolutely inside a positioned
|
|
40
|
+
parent. Two colour variants ship; scale/position with inline styles. */
|
|
41
|
+
.ui-glow {
|
|
42
|
+
position: absolute;
|
|
43
|
+
border-radius: 50%;
|
|
44
|
+
filter: blur(100px);
|
|
45
|
+
opacity: 0.5;
|
|
46
|
+
pointer-events: none;
|
|
47
|
+
z-index: 0;
|
|
48
|
+
width: 420px;
|
|
49
|
+
height: 420px;
|
|
50
|
+
}
|
|
51
|
+
.ui-glow--purple { background: radial-gradient(circle, var(--purple-light), transparent 70%); }
|
|
52
|
+
.ui-glow--green { background: radial-gradient(circle, var(--green), transparent 70%); opacity: 0.28; }
|
|
53
|
+
.ui-glow--cyan { background: radial-gradient(circle, var(--cyan), transparent 70%); opacity: 0.3; }
|
|
54
|
+
|
|
55
|
+
/* ----------------------------------------------------------------------------
|
|
56
|
+
* Background treatments — drop-in page/section backdrops. Each is a ::before
|
|
57
|
+
* layer, so add the class to a positioned wrapper and give the real content a
|
|
58
|
+
* higher stacking context (position:relative;z-index:1). All read the accent /
|
|
59
|
+
* glow tokens, so they re-theme with the sub-theme and work in dark + light.
|
|
60
|
+
* -------------------------------------------------------------------------- */
|
|
61
|
+
[class*="ui-bg-"] { position: relative; }
|
|
62
|
+
[class*="ui-bg-"] > * { position: relative; z-index: 1; }
|
|
63
|
+
[class*="ui-bg-"]::before { content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none; }
|
|
64
|
+
|
|
65
|
+
/* Spotlight — a soft radial from the top-centre (auth cards, focused heroes). */
|
|
66
|
+
.ui-bg-spotlight::before {
|
|
67
|
+
background: radial-gradient(620px 340px at 50% -12%, var(--glow-purple), transparent 70%);
|
|
68
|
+
}
|
|
69
|
+
/* Aurora — layered accent + cyan glows for a rich hero backdrop. */
|
|
70
|
+
.ui-bg-aurora::before {
|
|
71
|
+
background:
|
|
72
|
+
radial-gradient(520px 400px at 10% -12%, var(--glow-purple), transparent 60%),
|
|
73
|
+
radial-gradient(460px 360px at 94% 6%, var(--glow-cyan), transparent 62%);
|
|
74
|
+
}
|
|
75
|
+
/* Grid — a faint token-coloured grid, masked to fade at the edges. */
|
|
76
|
+
.ui-bg-grid::before {
|
|
77
|
+
background-image:
|
|
78
|
+
linear-gradient(to right, var(--border) 1px, transparent 1px),
|
|
79
|
+
linear-gradient(to bottom, var(--border) 1px, transparent 1px);
|
|
80
|
+
background-size: 34px 34px;
|
|
81
|
+
-webkit-mask-image: radial-gradient(circle at 50% 35%, #000, transparent 78%);
|
|
82
|
+
mask-image: radial-gradient(circle at 50% 35%, #000, transparent 78%);
|
|
83
|
+
}
|
|
84
|
+
/* Dots — a subtle dot field for dashboards / technical surfaces. */
|
|
85
|
+
.ui-bg-dots::before {
|
|
86
|
+
background-image: radial-gradient(var(--border) 1.4px, transparent 1.4px);
|
|
87
|
+
background-size: 22px 22px;
|
|
88
|
+
-webkit-mask-image: radial-gradient(circle at 50% 35%, #000, transparent 80%);
|
|
89
|
+
mask-image: radial-gradient(circle at 50% 35%, #000, transparent 80%);
|
|
90
|
+
}
|
|
91
|
+
/* Accent wash — a gentle top-down tint in the current accent. */
|
|
92
|
+
.ui-bg-wash::before {
|
|
93
|
+
background: linear-gradient(180deg, var(--glow-purple), transparent 45%);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Layout helpers used by the example apps */
|
|
97
|
+
.ui-container { width: 100%; max-width: 1180px; margin: 0 auto; padding: 0 clamp(14px, 2.4vw, 26px); }
|
|
98
|
+
.ui-stack > * + * { margin-top: var(--space-4); }
|
|
99
|
+
|
|
100
|
+
/* Section eyebrow — uppercase caption used across cards and headers */
|
|
101
|
+
.ui-eyebrow {
|
|
102
|
+
font-size: var(--text-xs);
|
|
103
|
+
letter-spacing: var(--tracking-caps);
|
|
104
|
+
text-transform: uppercase;
|
|
105
|
+
color: var(--muted);
|
|
106
|
+
font-weight: var(--weight-semibold);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Sensible default size for inline icons that a parent rule doesn't size.
|
|
110
|
+
Component rules (.ui-btn svg, .ui-card__icon svg…) come later and win the
|
|
111
|
+
tie, so they always override this — it only catches bare icon() calls. */
|
|
112
|
+
svg:not([width]):not([height]) {
|
|
113
|
+
width: 1.1em;
|
|
114
|
+
height: 1.1em;
|
|
115
|
+
flex: none;
|
|
116
|
+
vertical-align: -0.15em;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Focus-visible ring, shared by all interactive controls */
|
|
120
|
+
.ui-focusable:focus-visible,
|
|
121
|
+
.ui-btn:focus-visible,
|
|
122
|
+
.ui-input:focus-visible,
|
|
123
|
+
.ui-seg button:focus-visible,
|
|
124
|
+
.ui-switch:focus-visible {
|
|
125
|
+
outline: none;
|
|
126
|
+
box-shadow: var(--ring);
|
|
127
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* Button — .ui-btn (variants: primary / secondary / ghost / danger)
|
|
3
|
+
* (sizes: sm / md / lg, [icon], [disabled], [aria-busy])
|
|
4
|
+
* ========================================================================== */
|
|
5
|
+
|
|
6
|
+
.ui-btn {
|
|
7
|
+
--btn-pad-y: 9px;
|
|
8
|
+
--btn-pad-x: 17px;
|
|
9
|
+
--btn-font: var(--text-sm);
|
|
10
|
+
|
|
11
|
+
display: inline-flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
gap: 8px;
|
|
15
|
+
font-family: var(--font-sans);
|
|
16
|
+
font-size: var(--btn-font);
|
|
17
|
+
font-weight: var(--weight-medium);
|
|
18
|
+
line-height: 1;
|
|
19
|
+
border-radius: var(--radius-sm);
|
|
20
|
+
padding: var(--btn-pad-y) var(--btn-pad-x);
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
border: 1px solid var(--border);
|
|
23
|
+
background: var(--surface-2);
|
|
24
|
+
color: var(--text);
|
|
25
|
+
white-space: nowrap;
|
|
26
|
+
transition: background var(--dur-fast) var(--ease),
|
|
27
|
+
border-color var(--dur-fast) var(--ease),
|
|
28
|
+
color var(--dur-fast) var(--ease),
|
|
29
|
+
transform var(--dur-fast) var(--ease),
|
|
30
|
+
filter var(--dur-fast) var(--ease);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.ui-btn:hover {
|
|
34
|
+
border-color: var(--accent);
|
|
35
|
+
background: color-mix(in srgb, var(--accent) 14%, var(--surface-2));
|
|
36
|
+
color: var(--strong);
|
|
37
|
+
}
|
|
38
|
+
.ui-btn:active { transform: translateY(1px); }
|
|
39
|
+
|
|
40
|
+
.ui-btn svg { width: 16px; height: 16px; flex: none; }
|
|
41
|
+
|
|
42
|
+
/* -- Variants ----------------------------------------------------------- */
|
|
43
|
+
.ui-btn--primary {
|
|
44
|
+
background: var(--accent-strong);
|
|
45
|
+
color: var(--accent-contrast);
|
|
46
|
+
border-color: var(--accent-strong);
|
|
47
|
+
font-weight: var(--weight-semibold);
|
|
48
|
+
}
|
|
49
|
+
.ui-btn--primary:hover {
|
|
50
|
+
filter: brightness(1.1);
|
|
51
|
+
color: var(--accent-contrast);
|
|
52
|
+
background: var(--accent-strong);
|
|
53
|
+
border-color: var(--accent-strong);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.ui-btn--ghost {
|
|
57
|
+
background: transparent;
|
|
58
|
+
border-color: transparent;
|
|
59
|
+
color: var(--dim);
|
|
60
|
+
}
|
|
61
|
+
.ui-btn--ghost:hover {
|
|
62
|
+
background: var(--surface-2);
|
|
63
|
+
border-color: transparent;
|
|
64
|
+
color: var(--strong);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.ui-btn--danger { color: var(--muted); }
|
|
68
|
+
.ui-btn--danger:hover {
|
|
69
|
+
color: var(--pink);
|
|
70
|
+
border-color: var(--pink);
|
|
71
|
+
background: color-mix(in srgb, var(--pink) 10%, transparent);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* -- Sizes -------------------------------------------------------------- */
|
|
75
|
+
.ui-btn--sm { --btn-pad-y: 6px; --btn-pad-x: 12px; --btn-font: 12.5px; }
|
|
76
|
+
.ui-btn--lg { --btn-pad-y: 13px; --btn-pad-x: 24px; --btn-font: var(--text-md); border-radius: var(--radius-md); }
|
|
77
|
+
|
|
78
|
+
/* Icon-only */
|
|
79
|
+
.ui-btn--icon { padding: 9px; border-radius: var(--radius-sm); }
|
|
80
|
+
.ui-btn--icon.ui-btn--sm { padding: 6px; }
|
|
81
|
+
|
|
82
|
+
.ui-btn--block { width: 100%; }
|
|
83
|
+
|
|
84
|
+
/* -- States ------------------------------------------------------------- */
|
|
85
|
+
.ui-btn:disabled,
|
|
86
|
+
.ui-btn[aria-disabled="true"] {
|
|
87
|
+
opacity: 0.45;
|
|
88
|
+
cursor: not-allowed;
|
|
89
|
+
pointer-events: none;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Busy: keep the label, run a gradient-bar loader along the base, and the
|
|
93
|
+
button is disabled (not clickable). Pass busy + the .ui-btn__bars markup. */
|
|
94
|
+
.ui-btn[aria-busy="true"] {
|
|
95
|
+
pointer-events: none;
|
|
96
|
+
position: relative;
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
}
|
|
99
|
+
.ui-btn__bars {
|
|
100
|
+
position: absolute;
|
|
101
|
+
left: 0; right: 0; bottom: 0;
|
|
102
|
+
height: 3px;
|
|
103
|
+
background: color-mix(in srgb, currentColor 20%, transparent);
|
|
104
|
+
overflow: hidden;
|
|
105
|
+
}
|
|
106
|
+
.ui-btn__bars i {
|
|
107
|
+
position: absolute;
|
|
108
|
+
height: 100%;
|
|
109
|
+
border-radius: 3px;
|
|
110
|
+
background: linear-gradient(90deg, var(--purple-light), var(--cyan));
|
|
111
|
+
}
|
|
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
|
+
|
|
118
|
+
@keyframes ui-spin { to { transform: rotate(360deg); } }
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* Callout / Note / Toast — inline messages & transient notifications
|
|
3
|
+
* ========================================================================== */
|
|
4
|
+
|
|
5
|
+
.ui-callout {
|
|
6
|
+
display: flex;
|
|
7
|
+
gap: 12px;
|
|
8
|
+
padding: 15px 17px;
|
|
9
|
+
border-radius: var(--radius-md);
|
|
10
|
+
background: var(--surface-2);
|
|
11
|
+
font-size: 13.5px;
|
|
12
|
+
line-height: 1.55;
|
|
13
|
+
color: var(--dim);
|
|
14
|
+
}
|
|
15
|
+
.ui-callout__icon { width: 18px; height: 18px; flex: none; margin-top: 1px; color: var(--muted); }
|
|
16
|
+
.ui-callout__icon svg { width: 100%; height: 100%; stroke: currentColor; fill: none; stroke-width: 1.8; }
|
|
17
|
+
.ui-callout b { color: var(--text); font-weight: var(--weight-semibold); }
|
|
18
|
+
.ui-callout code { font-family: var(--font-mono); font-size: 0.92em; color: var(--text); }
|
|
19
|
+
|
|
20
|
+
.ui-callout--info { background: var(--glow-cyan); }
|
|
21
|
+
.ui-callout--info .ui-callout__icon { color: var(--cyan); }
|
|
22
|
+
.ui-callout--success { background: var(--glow-green); }
|
|
23
|
+
.ui-callout--success .ui-callout__icon { color: var(--green); }
|
|
24
|
+
.ui-callout--warn { background: color-mix(in srgb, var(--amber) 13%, transparent); }
|
|
25
|
+
.ui-callout--warn .ui-callout__icon { color: var(--amber); }
|
|
26
|
+
.ui-callout--danger { background: var(--glow-pink); }
|
|
27
|
+
.ui-callout--danger .ui-callout__icon { color: var(--pink); }
|
|
28
|
+
|
|
29
|
+
/* Toast — a floating, dismissible notification */
|
|
30
|
+
.ui-toast {
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: 12px;
|
|
34
|
+
padding: 13px 16px;
|
|
35
|
+
border-radius: var(--radius-md);
|
|
36
|
+
background: var(--bg-elevated);
|
|
37
|
+
box-shadow: var(--shadow-lg);
|
|
38
|
+
color: var(--text);
|
|
39
|
+
font-size: var(--text-base);
|
|
40
|
+
max-width: 380px;
|
|
41
|
+
}
|
|
42
|
+
.ui-toast__icon { width: 20px; height: 20px; flex: none; display: grid; place-items: center; border-radius: 50%; }
|
|
43
|
+
.ui-toast__icon svg { width: 13px; height: 13px; }
|
|
44
|
+
.ui-toast--success .ui-toast__icon { background: var(--green); color: #0c0c0c; }
|
|
45
|
+
.ui-toast--danger .ui-toast__icon { background: var(--pink); color: #fff; }
|
|
46
|
+
.ui-toast__body { flex: 1; }
|
|
47
|
+
.ui-toast__title { font-weight: var(--weight-medium); color: var(--strong); }
|
|
48
|
+
.ui-toast__close { background: none; border: 0; color: var(--muted); cursor: pointer; padding: 2px; display: grid; place-items: center; }
|
|
49
|
+
.ui-toast__close:hover { color: var(--text); }
|
|
50
|
+
|
|
51
|
+
/* Success panel — centered confirmation, used after form submit */
|
|
52
|
+
.ui-success {
|
|
53
|
+
border-radius: var(--radius-lg);
|
|
54
|
+
padding: 30px;
|
|
55
|
+
text-align: center;
|
|
56
|
+
background: var(--glow-green);
|
|
57
|
+
}
|
|
58
|
+
.ui-success__check {
|
|
59
|
+
width: 48px; height: 48px;
|
|
60
|
+
border-radius: 50%;
|
|
61
|
+
background: var(--green);
|
|
62
|
+
color: #0c0c0c;
|
|
63
|
+
display: grid;
|
|
64
|
+
place-items: center;
|
|
65
|
+
margin: 0 auto 14px;
|
|
66
|
+
}
|
|
67
|
+
.ui-success__check svg { width: 22px; height: 22px; stroke: currentColor; fill: none; stroke-width: 2.4; }
|
|
68
|
+
.ui-success__title { color: var(--strong); font-size: var(--text-lg); font-weight: var(--weight-semibold); margin-bottom: 5px; }
|
|
69
|
+
.ui-success__sub { color: var(--dim); font-size: var(--text-base); }
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* Card — .ui-card (filled surface, no borders — the deck groups with
|
|
3
|
+
* surface + radius, never outlines)
|
|
4
|
+
* Rows: .ui-card__row / headers: .ui-card__title / .ui-card__sub
|
|
5
|
+
* ========================================================================== */
|
|
6
|
+
|
|
7
|
+
.ui-card {
|
|
8
|
+
background: var(--surface);
|
|
9
|
+
border-radius: var(--radius-xl);
|
|
10
|
+
padding: var(--space-6) 26px;
|
|
11
|
+
position: relative;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Vertical stacks space cards with the container's gap — see .ui-card-stack.
|
|
15
|
+
(No `.ui-card + .ui-card` margin: it leaks into grid/flex rows and misaligns.) */
|
|
16
|
+
.ui-card-stack { display: flex; flex-direction: column; gap: var(--space-5); }
|
|
17
|
+
|
|
18
|
+
.ui-card--pad-sm { padding: var(--space-5); }
|
|
19
|
+
.ui-card--pad-lg { padding: var(--space-8) var(--space-10); }
|
|
20
|
+
|
|
21
|
+
/* An accent-tinted card, e.g. a highlighted plan / success surface */
|
|
22
|
+
.ui-card--accent {
|
|
23
|
+
background: color-mix(in srgb, var(--accent) 9%, var(--surface));
|
|
24
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 22%, transparent);
|
|
25
|
+
}
|
|
26
|
+
.ui-card--live {
|
|
27
|
+
background: color-mix(in srgb, var(--green) 10%, var(--surface));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Interactive card (link / button semantics) */
|
|
31
|
+
.ui-card--interactive {
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
transition: transform var(--dur-fast) var(--ease), box-shadow var(--dur-med) var(--ease);
|
|
34
|
+
}
|
|
35
|
+
.ui-card--interactive:hover {
|
|
36
|
+
transform: translateY(-2px);
|
|
37
|
+
box-shadow: var(--shadow-md);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.ui-card__title {
|
|
41
|
+
color: var(--strong);
|
|
42
|
+
font-size: var(--text-lg);
|
|
43
|
+
font-weight: var(--weight-semibold);
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: 10px;
|
|
47
|
+
margin: 0 0 5px;
|
|
48
|
+
}
|
|
49
|
+
.ui-card__sub {
|
|
50
|
+
color: var(--dim);
|
|
51
|
+
font-size: var(--text-base);
|
|
52
|
+
line-height: var(--leading-snug);
|
|
53
|
+
margin-bottom: var(--space-5);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Setting rows: label + control, divided by a hairline */
|
|
57
|
+
.ui-card__row {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
justify-content: space-between;
|
|
61
|
+
gap: var(--space-5);
|
|
62
|
+
padding: var(--space-4) 2px;
|
|
63
|
+
}
|
|
64
|
+
.ui-card__row + .ui-card__row { border-top: 1px solid var(--border); }
|
|
65
|
+
.ui-card__row .lab { color: var(--strong); font-size: var(--text-md); font-weight: var(--weight-medium); }
|
|
66
|
+
.ui-card__row .hint { color: var(--dim); font-size: 13.5px; margin-top: 3px; }
|
|
67
|
+
|
|
68
|
+
/* Icon chip that leads a card header */
|
|
69
|
+
.ui-card__icon {
|
|
70
|
+
width: 38px;
|
|
71
|
+
height: 38px;
|
|
72
|
+
border-radius: var(--radius-md);
|
|
73
|
+
display: grid;
|
|
74
|
+
place-items: center;
|
|
75
|
+
background: var(--glow-purple);
|
|
76
|
+
color: var(--accent);
|
|
77
|
+
flex: none;
|
|
78
|
+
}
|
|
79
|
+
.ui-card__icon svg { width: 19px; height: 19px; stroke: currentColor; fill: none; stroke-width: 1.7; }
|