@apliteni/apliteni-ui 0.3.0 → 0.4.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 +3 -1
- package/src/components/index.js +69 -6
- package/src/components/topbar.js +4 -3
- package/src/index.css +1 -0
- package/src/inline.js +2 -0
- package/src/styles/aurora.css +93 -0
- package/src/styles/topbar.css +9 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apliteni/apliteni-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
"@storybook/blocks": "^8.6.14",
|
|
44
44
|
"@storybook/html": "^8.6.14",
|
|
45
45
|
"@storybook/html-vite": "^8.6.14",
|
|
46
|
+
"axe-core": "^4.12.1",
|
|
47
|
+
"jsdom": "^29.1.1",
|
|
46
48
|
"storybook": "^8.6.14",
|
|
47
49
|
"vite": "^5.4.11"
|
|
48
50
|
}
|
package/src/components/index.js
CHANGED
|
@@ -48,6 +48,28 @@ export function card({ title, sub, body = '', variant, pad, icon: ic } = {}) {
|
|
|
48
48
|
return `<div class="${cls}">${head}${body}</div>`;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// ---- Aurora — ambient warm backdrop --------------------------------------
|
|
52
|
+
// Layered blurred glow "blobs" + optional paper grain. Colours default to the
|
|
53
|
+
// accent glow tokens, so the field re-themes with the sub-theme. Drop it as the
|
|
54
|
+
// first child of a positioned wrapper (or use fixed: true for full-bleed) and
|
|
55
|
+
// give the real content a normal stacking context — it sits on top.
|
|
56
|
+
const AURORA_PRESET = [
|
|
57
|
+
{ tone: 'accent', x: '22%', y: '12%', size: '64%', delay: '0s' },
|
|
58
|
+
{ tone: 'teal', x: '84%', y: '24%', size: '56%', delay: '-9s' },
|
|
59
|
+
{ tone: 'warm', x: '54%', y: '92%', size: '70%', delay: '-17s' },
|
|
60
|
+
];
|
|
61
|
+
export function aurora({ blobs = AURORA_PRESET, grain = false, fixed = false, animated = true, className = '' } = {}) {
|
|
62
|
+
const items = blobs.map((b) => {
|
|
63
|
+
const tone = b.tone || 'accent';
|
|
64
|
+
const style = `--au-x:${b.x ?? '50%'};--au-y:${b.y ?? '50%'};--au-size:${b.size ?? '60%'}`
|
|
65
|
+
+ (b.delay != null ? `;--au-delay:${b.delay}` : '');
|
|
66
|
+
return `<span class="ui-aurora__blob ui-aurora__blob--${tone}" style="${style}"></span>`;
|
|
67
|
+
}).join('');
|
|
68
|
+
const grainEl = grain ? '<span class="ui-aurora__grain"></span>' : '';
|
|
69
|
+
const cls = cx('ui-aurora', animated && 'ui-aurora--animated', fixed && 'ui-aurora--fixed', className);
|
|
70
|
+
return `<div class="${cls}" aria-hidden="true">${items}${grainEl}</div>`;
|
|
71
|
+
}
|
|
72
|
+
|
|
51
73
|
// ---- Segmented control ---------------------------------------------------
|
|
52
74
|
export function segmented({ options = [], active = 0, size, block, name = 'seg' } = {}) {
|
|
53
75
|
const cls = cx('ui-seg', size && `ui-seg--${size}`, block && 'ui-seg--block');
|
|
@@ -73,16 +95,57 @@ export function accentPicker({ active = 'default', options = ['default', 'phoeni
|
|
|
73
95
|
}
|
|
74
96
|
|
|
75
97
|
// ---- Field / input -------------------------------------------------------
|
|
76
|
-
|
|
77
|
-
|
|
98
|
+
// Auto-generated ids let field() tie its <label for> to the control it wraps.
|
|
99
|
+
// A module counter (never Date/random) keeps ids unique within a rendered page.
|
|
100
|
+
let _uid = 0;
|
|
101
|
+
const nextId = (p = 'ui') => `${p}-${++_uid}`;
|
|
102
|
+
// Give the first form control in `html` an id (or reuse one it already carries)
|
|
103
|
+
// so a <label> can point at it. Returns { html, id }; id is null if there's no
|
|
104
|
+
// form element to name.
|
|
105
|
+
function withControlId(html) {
|
|
106
|
+
const existing = html.match(/<(?:input|textarea|select)\b[^>]*\bid="([^"]+)"/);
|
|
107
|
+
if (existing) return { html, id: existing[1] };
|
|
108
|
+
let id = null;
|
|
109
|
+
const out = html.replace(/<(input|textarea|select)\b/, (m) => { id = nextId('field'); return `${m} id="${id}"`; });
|
|
110
|
+
return { html: out, id };
|
|
78
111
|
}
|
|
79
|
-
|
|
80
|
-
|
|
112
|
+
|
|
113
|
+
export function field({ label, hint, error, control = '', id } = {}) {
|
|
114
|
+
let ctl = control;
|
|
115
|
+
let forId = id;
|
|
116
|
+
if (label && control) {
|
|
117
|
+
const r = withControlId(control);
|
|
118
|
+
ctl = r.html;
|
|
119
|
+
forId = id || r.id;
|
|
120
|
+
}
|
|
121
|
+
const lab = label
|
|
122
|
+
? `<label class="ui-field__label"${forId ? ` for="${forId}"` : ''}>${esc(label)}</label>`
|
|
123
|
+
: '';
|
|
124
|
+
const foot = error
|
|
125
|
+
? `<div class="ui-field__error">${icon('alert')}${esc(error)}</div>`
|
|
126
|
+
: hint ? `<div class="ui-field__hint">${esc(hint)}</div>` : '';
|
|
127
|
+
return `<div class="ui-field">${lab}${ctl}${foot}</div>`;
|
|
128
|
+
}
|
|
129
|
+
// `ariaLabel` gives a standalone control (no wrapping field) an accessible name.
|
|
130
|
+
export function input({ type = 'text', placeholder = '', value = '', icon: ic, invalid, disabled, name, id, ariaLabel } = {}) {
|
|
131
|
+
const attrs = `${id ? ` id="${esc(id)}"` : ''}${name ? ` name="${name}"` : ''}${ariaLabel ? ` aria-label="${esc(ariaLabel)}"` : ''}${disabled ? ' disabled' : ''}`;
|
|
132
|
+
const el = `<input class="${cx('ui-input', invalid && 'is-invalid')}" type="${type}" placeholder="${esc(placeholder)}" value="${esc(value)}"${attrs}>`;
|
|
81
133
|
if (!ic) return el;
|
|
82
134
|
return `<div class="ui-input-group"><span class="ui-input-group__icon">${icon(ic)}</span>${el}</div>`;
|
|
83
135
|
}
|
|
84
|
-
export function textarea({ placeholder = '', value = '', rows = 4 } = {}) {
|
|
85
|
-
return `<textarea class="ui-textarea" rows="${rows}" placeholder="${esc(placeholder)}">${esc(value)}</textarea>`;
|
|
136
|
+
export function textarea({ placeholder = '', value = '', rows = 4, name, id, ariaLabel } = {}) {
|
|
137
|
+
return `<textarea class="ui-textarea" rows="${rows}" placeholder="${esc(placeholder)}"${id ? ` id="${esc(id)}"` : ''}${name ? ` name="${name}"` : ''}${ariaLabel ? ` aria-label="${esc(ariaLabel)}"` : ''}>${esc(value)}</textarea>`;
|
|
138
|
+
}
|
|
139
|
+
// Native <select>. Pass a `label` via field() or an `ariaLabel` for a bare one —
|
|
140
|
+
// a select with neither has no accessible name.
|
|
141
|
+
export function select({ options = [], value, name, id, ariaLabel, disabled } = {}) {
|
|
142
|
+
const opts = options.map((o) => {
|
|
143
|
+
const label = typeof o === 'string' ? o : o.label;
|
|
144
|
+
const val = typeof o === 'string' ? o : (o.value ?? o.label);
|
|
145
|
+
const sel = value != null && String(val) === String(value);
|
|
146
|
+
return `<option value="${esc(val)}"${sel ? ' selected' : ''}>${esc(label)}</option>`;
|
|
147
|
+
}).join('');
|
|
148
|
+
return `<select class="ui-select"${id ? ` id="${esc(id)}"` : ''}${name ? ` name="${name}"` : ''}${ariaLabel ? ` aria-label="${esc(ariaLabel)}"` : ''}${disabled ? ' disabled' : ''}>${opts}</select>`;
|
|
86
149
|
}
|
|
87
150
|
export function checkbox({ label, checked, type = 'checkbox', name } = {}) {
|
|
88
151
|
return `<label class="ui-check"><input type="${type}"${name ? ` name="${name}"` : ''}${checked ? ' checked' : ''}><span>${label}</span></label>`;
|
package/src/components/topbar.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// (brand, Deck/Text, theme toggle, version switcher, account menu).
|
|
3
3
|
import { brand } from '../assets/brand.js';
|
|
4
4
|
import { icon, sun, moon } from '../assets/icons.js';
|
|
5
|
+
import { esc } from './index.js';
|
|
5
6
|
|
|
6
7
|
const THEME_KEY = 'apliteni-strategy-theme';
|
|
7
8
|
|
|
@@ -18,12 +19,12 @@ export function deckTextSwitch(active = 'deck') {
|
|
|
18
19
|
export function versionSwitcher(versions = [], activeIdx = 0) {
|
|
19
20
|
const cur = versions[activeIdx]?.label || '';
|
|
20
21
|
const opts = versions.map((v, i) =>
|
|
21
|
-
`<div class="vopt" role="option" data-active="${i === activeIdx ? '1' : '0'}">` +
|
|
22
|
+
`<div class="vopt" role="option" aria-selected="${i === activeIdx}" data-active="${i === activeIdx ? '1' : '0'}">` +
|
|
22
23
|
`<span><div class="vname">${v.label}</div><div class="vmeta">${v.meta || ''}</div></span>` +
|
|
23
24
|
`<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
|
+
return `<div class="vsw" data-vsw><button class="vsw__btn" aria-haspopup="listbox" aria-expanded="false" aria-label="Version — ${esc(cur)}">` +
|
|
25
26
|
`<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
|
+
`<div class="vsw__menu" role="listbox" aria-label="Version">${opts}</div></div>`;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
// `nav` ([id, icon, label, href?, target?][]) mirrors the account sidebar so the
|
package/src/index.css
CHANGED
package/src/inline.js
CHANGED
|
@@ -24,6 +24,7 @@ export const topbarCss = read('styles/topbar.css');
|
|
|
24
24
|
// Individual component stylesheets, addressable by name.
|
|
25
25
|
export const styles = {
|
|
26
26
|
base: baseCss,
|
|
27
|
+
aurora: read('styles/aurora.css'),
|
|
27
28
|
button: read('styles/button.css'),
|
|
28
29
|
card: read('styles/card.css'),
|
|
29
30
|
badge: read('styles/badge.css'),
|
|
@@ -41,6 +42,7 @@ export const styles = {
|
|
|
41
42
|
export const cssText = [
|
|
42
43
|
tokensCss,
|
|
43
44
|
styles.base,
|
|
45
|
+
styles.aurora,
|
|
44
46
|
styles.button,
|
|
45
47
|
styles.card,
|
|
46
48
|
styles.badge,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* aurora — ambient warm backdrop: layered blurred glow "blobs" + optional
|
|
3
|
+
* paper grain. The kit's one signature page background, so every app gets one
|
|
4
|
+
* warm identity for free.
|
|
5
|
+
*
|
|
6
|
+
* Markup comes from aurora() in components/index.js, but you can hand-roll it:
|
|
7
|
+
* <div class="ui-aurora ui-aurora--animated" aria-hidden="true">
|
|
8
|
+
* <span class="ui-aurora__blob ui-aurora__blob--accent" style="--au-x:12%;--au-y:-4%"></span>
|
|
9
|
+
* ...
|
|
10
|
+
* <span class="ui-aurora__grain"></span>
|
|
11
|
+
* </div>
|
|
12
|
+
*
|
|
13
|
+
* Colours default to the accent glow tokens, so the whole field re-themes with
|
|
14
|
+
* the sub-theme (phoenix / ocean / emerald) and holds up in dark AND light with
|
|
15
|
+
* no per-accent rules. Override --au-1/2/3 on .ui-aurora to retune.
|
|
16
|
+
* ========================================================================== */
|
|
17
|
+
|
|
18
|
+
.ui-aurora {
|
|
19
|
+
/* Tone slots — mixed to a visible alpha off the accent families so the field
|
|
20
|
+
actually reads as an aurora (the --glow-* tokens are tuned faint for the
|
|
21
|
+
small .ui-bg-* utilities). --purple-light + --grad-to re-point per accent,
|
|
22
|
+
so the whole field re-themes; --cyan stays a fixed teal. Override to retune. */
|
|
23
|
+
--au-1: color-mix(in srgb, var(--purple-light) 52%, transparent);
|
|
24
|
+
--au-2: color-mix(in srgb, var(--cyan) 40%, transparent);
|
|
25
|
+
--au-3: color-mix(in srgb, var(--grad-to) 44%, transparent);
|
|
26
|
+
|
|
27
|
+
position: absolute;
|
|
28
|
+
inset: 0;
|
|
29
|
+
z-index: 0;
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
pointer-events: none;
|
|
32
|
+
}
|
|
33
|
+
/* Light theme: the accent families are darker solids, so dial the mix right
|
|
34
|
+
down — a whisper of tint, not a wash. */
|
|
35
|
+
:root[data-theme="light"] .ui-aurora {
|
|
36
|
+
--au-1: color-mix(in srgb, var(--purple-light) 18%, transparent);
|
|
37
|
+
--au-2: color-mix(in srgb, var(--cyan) 15%, transparent);
|
|
38
|
+
--au-3: color-mix(in srgb, var(--grad-to) 17%, transparent);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Full-bleed: pin to the viewport behind the whole page. Give real content a
|
|
42
|
+
normal (non-negative) stacking context and it sits on top automatically. */
|
|
43
|
+
.ui-aurora--fixed {
|
|
44
|
+
position: fixed;
|
|
45
|
+
z-index: -1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* A blurred radial blob. Position via --au-x / --au-y (its centre), scale via
|
|
49
|
+
--au-size. Centring uses `translate` so the drift animation owns `transform`. */
|
|
50
|
+
.ui-aurora__blob {
|
|
51
|
+
position: absolute;
|
|
52
|
+
top: var(--au-y, 50%);
|
|
53
|
+
left: var(--au-x, 50%);
|
|
54
|
+
width: var(--au-size, 60%);
|
|
55
|
+
aspect-ratio: 1;
|
|
56
|
+
translate: -50% -50%;
|
|
57
|
+
border-radius: 50%;
|
|
58
|
+
background: radial-gradient(circle at center, var(--au-tone), transparent 68%);
|
|
59
|
+
filter: blur(70px);
|
|
60
|
+
will-change: transform;
|
|
61
|
+
}
|
|
62
|
+
.ui-aurora__blob--accent { --au-tone: var(--au-1); }
|
|
63
|
+
.ui-aurora__blob--teal { --au-tone: var(--au-2); }
|
|
64
|
+
.ui-aurora__blob--warm { --au-tone: var(--au-3); }
|
|
65
|
+
|
|
66
|
+
/* Slow ambient drift. Staggered per blob via --au-delay so they never line up. */
|
|
67
|
+
.ui-aurora--animated .ui-aurora__blob {
|
|
68
|
+
animation: ui-aurora-drift 26s ease-in-out infinite alternate;
|
|
69
|
+
animation-delay: var(--au-delay, 0s);
|
|
70
|
+
}
|
|
71
|
+
@keyframes ui-aurora-drift {
|
|
72
|
+
from { transform: translate(-2%, -1%) scale(1); }
|
|
73
|
+
to { transform: translate(3%, 2%) scale(1.12); }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Paper grain — a low-opacity fractal-noise veil. `overlay` in dark keeps it
|
|
77
|
+
from muddying the blacks; `multiply` in light reads as fine tooth on paper. */
|
|
78
|
+
.ui-aurora__grain {
|
|
79
|
+
position: absolute;
|
|
80
|
+
inset: 0;
|
|
81
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
|
82
|
+
background-size: 160px 160px;
|
|
83
|
+
opacity: 0.5;
|
|
84
|
+
mix-blend-mode: overlay;
|
|
85
|
+
}
|
|
86
|
+
:root[data-theme="light"] .ui-aurora__grain {
|
|
87
|
+
opacity: 0.4;
|
|
88
|
+
mix-blend-mode: multiply;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@media (prefers-reduced-motion: reduce) {
|
|
92
|
+
.ui-aurora__blob { animation: none; }
|
|
93
|
+
}
|
package/src/styles/topbar.css
CHANGED
|
@@ -21,16 +21,22 @@
|
|
|
21
21
|
align-items: center;
|
|
22
22
|
gap: 14px;
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
/* Brand lockup (mark + wordmark) — self-contained so the mark stays optically
|
|
25
|
+
centered with the word, with an even gap, in ANY context (topbar, auth cards,
|
|
26
|
+
consent). These rules used to be scoped to `.topbar`, so a lockup dropped into
|
|
27
|
+
the consent/auth card fell back to inline layout: the mark rode the text
|
|
28
|
+
baseline with no gap between it and the word. */
|
|
29
|
+
.brand {
|
|
25
30
|
display: inline-flex;
|
|
26
31
|
align-items: center;
|
|
27
32
|
gap: 9px;
|
|
28
|
-
font-size: 13px;
|
|
29
33
|
font-weight: 600;
|
|
30
34
|
color: var(--strong);
|
|
31
35
|
text-decoration: none;
|
|
32
36
|
}
|
|
33
|
-
.
|
|
37
|
+
.brand svg { display: block; flex: none; }
|
|
38
|
+
/* Topbar wordmark runs a touch smaller than the auth-card lockup. */
|
|
39
|
+
.topbar .brand { font-size: 13px; }
|
|
34
40
|
.topbar .spacer { flex: 1; }
|
|
35
41
|
|
|
36
42
|
/* Deck / Text (segmented, pill) */
|