@apliteni/apliteni-ui 0.7.1 → 0.8.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/LICENSE +20 -5
- package/README.md +88 -19
- package/package.json +21 -12
- package/react/README.md +47 -0
- package/react/dist/index.css +76 -0
- package/react/dist/index.d.ts +58 -0
- package/react/dist/index.js +254 -0
- package/src/assets/icons.js +95 -72
- package/src/components/feedback.js +9 -7
- package/src/components/index.js +90 -15
- package/src/components/tabs.js +6 -2
- package/src/components/topbar.js +39 -5
- package/src/styles/badge.css +11 -13
- package/src/styles/callout.css +5 -5
- package/src/styles/card.css +1 -1
- package/src/styles/drawer.css +2 -4
- package/src/styles/dropdown.css +1 -1
- package/src/styles/empty.css +1 -1
- package/src/styles/feedback.css +12 -6
- package/src/styles/input.css +4 -1
- package/src/styles/layout.css +1 -1
- package/src/styles/motion.css +1 -1
- package/src/styles/segmented.css +3 -0
- package/src/styles/tabs.css +3 -0
- package/src/styles/topbar.css +3 -3
- package/src/tokens/accents.css +6 -2
- package/src/tokens/brand.generated.css +1 -1
- package/src/tokens/tokens.css +54 -0
package/src/components/index.js
CHANGED
|
@@ -11,6 +11,11 @@ export const esc = (s) => String(s == null ? '' : s).replace(/[&<>"]/g, (c) => (
|
|
|
11
11
|
// branded glyphs the kit's icon set doesn't own — e.g. a Google "G". It takes
|
|
12
12
|
// the leading slot; `icon` is the named-icon shorthand for kit glyphs. This is
|
|
13
13
|
// what lets third-party buttons go through button() instead of hand-rolled markup.
|
|
14
|
+
//
|
|
15
|
+
// `iconOnly` drops the visible text, and kit glyphs are aria-hidden — so `label`
|
|
16
|
+
// stops being decoration and becomes the control's ONLY accessible name. It is
|
|
17
|
+
// mirrored into aria-label + title, and an empty label falls back to the icon
|
|
18
|
+
// name rather than shipping a nameless button.
|
|
14
19
|
export function button({
|
|
15
20
|
label = 'Button', variant = 'secondary', size = 'md', icon: ic, iconSvg, iconRight,
|
|
16
21
|
block = false, disabled = false, busy = false, type = 'button', href, iconOnly = false,
|
|
@@ -26,7 +31,9 @@ export function button({
|
|
|
26
31
|
const lead = iconSvg || (ic ? icon(ic) : '');
|
|
27
32
|
const inner = `${lead}${iconOnly ? '' : `<span>${esc(label)}</span>`}${iconRight ? icon(iconRight) : ''}${bars}`;
|
|
28
33
|
// busy ⇒ disabled (not clickable while it works)
|
|
29
|
-
const
|
|
34
|
+
const name = String(label == null ? '' : label).trim() || ic || 'Button';
|
|
35
|
+
const named = iconOnly ? ` aria-label="${esc(name)}" title="${esc(name)}"` : '';
|
|
36
|
+
const attrs = `class="${cls}"${disabled || busy ? ' disabled aria-disabled="true"' : ''}${busy ? ' aria-busy="true"' : ''}${named}`;
|
|
30
37
|
return href
|
|
31
38
|
? `<a href="${href}" ${attrs}>${inner}</a>`
|
|
32
39
|
: `<button type="${type}" ${attrs}>${inner}</button>`;
|
|
@@ -55,15 +62,34 @@ export function card({ title, sub, body = '', variant, pad, icon: ic } = {}) {
|
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
// ---- Segmented control ---------------------------------------------------
|
|
58
|
-
|
|
65
|
+
// A strip of mutually exclusive toggle buttons — a filter, a unit switch, a
|
|
66
|
+
// language picker. It is NOT a tablist: it controls no panel, so it must not
|
|
67
|
+
// announce one. `role="toolbar"` with a roving tabindex is the honest shape —
|
|
68
|
+
// one Tab stop for the whole strip, ArrowLeft/ArrowRight to move between the
|
|
69
|
+
// options, Home/End to jump to the ends, and `aria-pressed` to say which is on.
|
|
70
|
+
// wireTopbar() ships that keyboard behaviour (see topbar.js); the markup here
|
|
71
|
+
// carries the state it reads.
|
|
72
|
+
//
|
|
73
|
+
// When the pill drives a real change of view with content behind it, reach for
|
|
74
|
+
// tabs() instead — that one owns panels and earns the tab announcement.
|
|
75
|
+
//
|
|
76
|
+
// `ariaLabel` names the strip. `name` seeds data-seg (a hook for callers) and
|
|
77
|
+
// is deliberately not an accessible name — it is an identifier, not prose.
|
|
78
|
+
// Defaults to "Options" so a strip is never left unlabelled, the same way
|
|
79
|
+
// switchToggle() defaults its label.
|
|
80
|
+
export function segmented({ options = [], active = 0, size, block, name = 'seg', ariaLabel = 'Options' } = {}) {
|
|
59
81
|
const cls = cx('ui-seg', size && `ui-seg--${size}`, block && 'ui-seg--block');
|
|
82
|
+
// Exactly one option holds the Tab stop. If `active` points nowhere (nothing
|
|
83
|
+
// selected yet) the first option holds it, so the strip is never unreachable.
|
|
84
|
+
const rove = active >= 0 && active < options.length ? active : 0;
|
|
60
85
|
const btns = options.map((o, i) => {
|
|
61
86
|
const label = typeof o === 'string' ? o : o.label;
|
|
62
87
|
const val = typeof o === 'string' ? o : (o.value ?? o.label);
|
|
63
88
|
const on = i === active;
|
|
64
|
-
return `<button type="button"
|
|
89
|
+
return `<button type="button" aria-pressed="${on}" tabindex="${i === rove ? '0' : '-1'}"`
|
|
90
|
+
+ ` data-value="${esc(val)}"${on ? ' class="is-active"' : ''}>${esc(label)}</button>`;
|
|
65
91
|
}).join('');
|
|
66
|
-
return `<div class="${cls}" role="
|
|
92
|
+
return `<div class="${cls}" role="toolbar" aria-label="${esc(ariaLabel)}" data-seg="${name}">${btns}</div>`;
|
|
67
93
|
}
|
|
68
94
|
|
|
69
95
|
// ---- Accent picker -------------------------------------------------------
|
|
@@ -73,9 +99,14 @@ const ACCENT_SWATCH = {
|
|
|
73
99
|
ocean: 'linear-gradient(135deg,#5ab0ff,#3b9dff)',
|
|
74
100
|
emerald: 'linear-gradient(135deg,#3ad9a0,#16c98a)',
|
|
75
101
|
};
|
|
102
|
+
// Swatches have no text and no glyph at all — the aria-label is the whole
|
|
103
|
+
// accessible name, and aria-pressed carries which one is on.
|
|
76
104
|
export function accentPicker({ active = 'default', options = ['default', 'phoenix', 'ocean', 'emerald'] } = {}) {
|
|
77
|
-
|
|
78
|
-
|
|
105
|
+
const title = (o) => o.charAt(0).toUpperCase() + o.slice(1);
|
|
106
|
+
return `<div class="ui-accent-picker" data-accent-group role="group" aria-label="Accent">${options.map((o) =>
|
|
107
|
+
`<button type="button" data-accent-pick="${esc(o)}"${o === active ? ' class="is-active"' : ''}`
|
|
108
|
+
+ ` style="--swatch:${ACCENT_SWATCH[o] || 'transparent'}" aria-pressed="${o === active ? 'true' : 'false'}"`
|
|
109
|
+
+ ` aria-label="${esc(title(o))} accent" title="${esc(title(o))}"></button>`).join('')}</div>`;
|
|
79
110
|
}
|
|
80
111
|
|
|
81
112
|
// ---- Field / input -------------------------------------------------------
|
|
@@ -93,8 +124,32 @@ function withControlId(html) {
|
|
|
93
124
|
const out = html.replace(/<(input|textarea|select)\b/, (m) => { id = nextId('field'); return `${m} id="${id}"`; });
|
|
94
125
|
return { html: out, id };
|
|
95
126
|
}
|
|
127
|
+
// Same trick, for the attributes that carry state rather than identity:
|
|
128
|
+
// aria-describedby, aria-invalid, required. Anything the control already spells
|
|
129
|
+
// out for itself wins — a caller who passed `invalid` to input() keeps their own
|
|
130
|
+
// aria-invalid. `true` renders a bare boolean attribute.
|
|
131
|
+
function withControlAttrs(html, attrs) {
|
|
132
|
+
const pairs = Object.entries(attrs).filter(([, v]) => v != null && v !== false);
|
|
133
|
+
if (!pairs.length) return html;
|
|
134
|
+
return html.replace(/<(input|textarea|select)\b([^>]*?)(\s*\/?)>/, (m, tag, rest, tail) => {
|
|
135
|
+
const add = pairs
|
|
136
|
+
.filter(([k]) => !new RegExp(`\\s${k}(?=[\\s=>/]|$)`).test(rest))
|
|
137
|
+
.map(([k, v]) => (v === true ? ` ${k}` : ` ${k}="${esc(v)}"`))
|
|
138
|
+
.join('');
|
|
139
|
+
return `<${tag}${rest}${add}${tail}>`;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
96
142
|
|
|
97
|
-
|
|
143
|
+
// A labelled control with, optionally, a hint or an error under it.
|
|
144
|
+
//
|
|
145
|
+
// The label half was wired in #119 (withControlId above → a real `for=`). This
|
|
146
|
+
// finishes the job for the rest: the hint or error gets an id and the control
|
|
147
|
+
// points at it with aria-describedby, so the reason a value was rejected is read
|
|
148
|
+
// out with the field instead of sitting beside it as loose text. An errored
|
|
149
|
+
// control is marked aria-invalid, and `required` puts the requirement in the
|
|
150
|
+
// markup rather than in the label's wording — the asterisk is decoration on top
|
|
151
|
+
// of that, so it is hidden from assistive technology.
|
|
152
|
+
export function field({ label, hint, error, control = '', id, required = false } = {}) {
|
|
98
153
|
let ctl = control;
|
|
99
154
|
let forId = id;
|
|
100
155
|
if (label && control) {
|
|
@@ -102,17 +157,29 @@ export function field({ label, hint, error, control = '', id } = {}) {
|
|
|
102
157
|
ctl = r.html;
|
|
103
158
|
forId = id || r.id;
|
|
104
159
|
}
|
|
160
|
+
// Error wins over hint — only one message shows, so only one id is minted.
|
|
161
|
+
const msg = error || hint;
|
|
162
|
+
const msgId = msg && control ? nextId(error ? 'field-err' : 'field-hint') : null;
|
|
163
|
+
ctl = withControlAttrs(ctl, {
|
|
164
|
+
'aria-describedby': msgId,
|
|
165
|
+
'aria-invalid': error ? 'true' : null,
|
|
166
|
+
required: required || null,
|
|
167
|
+
});
|
|
105
168
|
const lab = label
|
|
106
|
-
? `<label class="ui-field__label"${forId ? ` for="${forId}"` : ''}>${esc(label)}
|
|
169
|
+
? `<label class="ui-field__label"${forId ? ` for="${forId}"` : ''}>${esc(label)}`
|
|
170
|
+
+ `${required ? '<span class="ui-field__req" aria-hidden="true">*</span>' : ''}</label>`
|
|
107
171
|
: '';
|
|
108
172
|
const foot = error
|
|
109
|
-
? `<div class="ui-field__error">${icon('alert')}${esc(error)}</div>`
|
|
110
|
-
: hint ? `<div class="ui-field__hint">${esc(hint)}</div>` : '';
|
|
173
|
+
? `<div class="ui-field__error"${msgId ? ` id="${msgId}"` : ''}>${icon('alert')}${esc(error)}</div>`
|
|
174
|
+
: hint ? `<div class="ui-field__hint"${msgId ? ` id="${msgId}"` : ''}>${esc(hint)}</div>` : '';
|
|
111
175
|
return `<div class="ui-field">${lab}${ctl}${foot}</div>`;
|
|
112
176
|
}
|
|
113
177
|
// `ariaLabel` gives a standalone control (no wrapping field) an accessible name.
|
|
114
|
-
|
|
115
|
-
|
|
178
|
+
// `invalid` paints the control red AND says so in aria-invalid — the red on its
|
|
179
|
+
// own is a state only a sighted user can read.
|
|
180
|
+
export function input({ type = 'text', placeholder = '', value = '', icon: ic, invalid, disabled, required, name, id, ariaLabel } = {}) {
|
|
181
|
+
const attrs = `${id ? ` id="${esc(id)}"` : ''}${name ? ` name="${name}"` : ''}${ariaLabel ? ` aria-label="${esc(ariaLabel)}"` : ''}`
|
|
182
|
+
+ `${invalid ? ' aria-invalid="true"' : ''}${required ? ' required' : ''}${disabled ? ' disabled' : ''}`;
|
|
116
183
|
const el = `<input class="${cx('ui-input', invalid && 'is-invalid')}" type="${type}" placeholder="${esc(placeholder)}" value="${esc(value)}"${attrs}>`;
|
|
117
184
|
if (!ic) return el;
|
|
118
185
|
return `<div class="ui-input-group"><span class="ui-input-group__icon">${icon(ic)}</span>${el}</div>`;
|
|
@@ -158,7 +225,8 @@ export function toast({
|
|
|
158
225
|
const cls = cx('ui-toast', `ui-toast--${variant}`, `ui-toast--${style}`, compact && 'ui-toast--compact');
|
|
159
226
|
const actLabel = typeof action === 'string' ? action : action && action.label;
|
|
160
227
|
const actBtn = actLabel ? `<button class="ui-toast__action" type="button">${esc(actLabel)}</button>` : '';
|
|
161
|
-
|
|
228
|
+
// Icon-only: the glyph is aria-hidden, so aria-label IS the accessible name.
|
|
229
|
+
const closeBtn = dismissible ? `<button type="button" class="ui-toast__close" aria-label="Dismiss">${icon('x')}</button>` : '';
|
|
162
230
|
const bodyHtml = compact || !body ? '' : `<div class="ui-toast__text">${esc(body)}</div>`;
|
|
163
231
|
const dur = typeof timer === 'number' ? ` style="--toast-dur:${timer}s"` : '';
|
|
164
232
|
const timerBar = timer ? '<span class="ui-toast__timer" data-toast-timer></span>' : '';
|
|
@@ -187,8 +255,15 @@ export function emptyState({ art, icon: ic, title, sub, actions } = {}) {
|
|
|
187
255
|
}
|
|
188
256
|
|
|
189
257
|
// ---- Snippet -------------------------------------------------------------
|
|
190
|
-
|
|
191
|
-
|
|
258
|
+
// `copyLabel` is both the visible text and the accessible name of the copy
|
|
259
|
+
// button. wireTopbar() swaps that text to "✓ Copied" and back, so the button is
|
|
260
|
+
// never left nameless — the glyph beside it is decorative (aria-hidden).
|
|
261
|
+
// `type="button"`: without it a snippet dropped inside a <form> submits the form.
|
|
262
|
+
export function snippet({ label = 'shell', code = '', reveal = false, copy = true, copyLabel = 'Copy' } = {}) {
|
|
263
|
+
const copyBtn = copy
|
|
264
|
+
? `<button type="button" class="ui-snippet__copy" data-orig="${esc(copyLabel)}">${icon('copy')}${esc(copyLabel)}</button>`
|
|
265
|
+
: '';
|
|
266
|
+
return `<div class="${cx('ui-snippet', reveal && 'ui-snippet--reveal')}"><div class="ui-snippet__bar"><span>${esc(label)}</span>${copyBtn}</div><pre>${code}</pre></div>`;
|
|
192
267
|
}
|
|
193
268
|
|
|
194
269
|
// Tiny shell highlighter: escapes first, then wraps comments/strings/URLs/flags/command.
|
package/src/components/tabs.js
CHANGED
|
@@ -20,7 +20,7 @@ export function tabs({ items = [], active = 0, name = 'tabs', ariaLabel = 'Tabs'
|
|
|
20
20
|
const list = items
|
|
21
21
|
.map((it, i) => {
|
|
22
22
|
const on = i === active;
|
|
23
|
-
return `<button class="ui-tabs__tab${on ? ' is-active' : ''}" role="tab"`
|
|
23
|
+
return `<button type="button" class="ui-tabs__tab${on ? ' is-active' : ''}" role="tab"`
|
|
24
24
|
+ ` id="${name}-tab-${i}" aria-controls="${name}-panel-${i}"`
|
|
25
25
|
+ ` aria-selected="${on ? 'true' : 'false'}" tabindex="${on ? '0' : '-1'}">${it.label}</button>`;
|
|
26
26
|
})
|
|
@@ -28,8 +28,12 @@ export function tabs({ items = [], active = 0, name = 'tabs', ariaLabel = 'Tabs'
|
|
|
28
28
|
const panels = items
|
|
29
29
|
.map((it, i) => {
|
|
30
30
|
const on = i === active;
|
|
31
|
+
// tabindex="0" on the panel: the APG requires it when the panel holds no
|
|
32
|
+
// focusable content of its own, and most panels here are plain text. Without
|
|
33
|
+
// it a keyboard user reaches the tab strip and then cannot reach what it
|
|
34
|
+
// switched to — Tab jumps straight past the panel to whatever follows.
|
|
31
35
|
return `<div class="ui-tabs__panel" role="tabpanel" id="${name}-panel-${i}"`
|
|
32
|
-
+ ` aria-labelledby="${name}-tab-${i}"${on ? '' : ' hidden'}>${it.panel || ''}</div>`;
|
|
36
|
+
+ ` aria-labelledby="${name}-tab-${i}" tabindex="0"${on ? '' : ' hidden'}>${it.panel || ''}</div>`;
|
|
33
37
|
})
|
|
34
38
|
.join('');
|
|
35
39
|
return `<div class="${cls}" data-tabs>`
|
package/src/components/topbar.js
CHANGED
|
@@ -106,14 +106,44 @@ export function wireTopbar(root = document) {
|
|
|
106
106
|
a.setAttribute('aria-current', 'page');
|
|
107
107
|
});
|
|
108
108
|
});
|
|
109
|
-
// Segmented controls (.ui-seg) —
|
|
109
|
+
// Segmented controls (.ui-seg) — a toolbar of toggle buttons (see segmented()
|
|
110
|
+
// in components/index.js). Click selects; ArrowLeft/ArrowRight move and wrap,
|
|
111
|
+
// Home/End jump to the ends. The strip keeps ONE Tab stop: selecting an option
|
|
112
|
+
// hands it the tabindex and takes it off the rest, so a page with three of
|
|
113
|
+
// these costs three Tab presses, not nine.
|
|
110
114
|
root.querySelectorAll('.ui-seg').forEach((seg) => {
|
|
115
|
+
const btns = () => Array.prototype.slice.call(seg.querySelectorAll('button'));
|
|
116
|
+
const select = (b, focus) => {
|
|
117
|
+
btns().forEach((x) => {
|
|
118
|
+
const on = x === b;
|
|
119
|
+
x.classList.toggle('is-active', on);
|
|
120
|
+
// Older hand-written .ui-seg markup carries aria-selected; segmented()
|
|
121
|
+
// now emits aria-pressed. Keep whichever the button actually declares in
|
|
122
|
+
// step, and never invent the other one.
|
|
123
|
+
if (x.hasAttribute('aria-pressed')) x.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
124
|
+
if (x.hasAttribute('aria-selected')) x.setAttribute('aria-selected', on ? 'true' : 'false');
|
|
125
|
+
x.tabIndex = on ? 0 : -1;
|
|
126
|
+
});
|
|
127
|
+
if (focus) b.focus();
|
|
128
|
+
};
|
|
111
129
|
seg.addEventListener('click', (e) => {
|
|
112
130
|
const b = e.target.closest('button');
|
|
113
131
|
if (!b || !seg.contains(b)) return;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
132
|
+
select(b);
|
|
133
|
+
});
|
|
134
|
+
seg.addEventListener('keydown', (e) => {
|
|
135
|
+
const b = e.target.closest('button');
|
|
136
|
+
if (!b || !seg.contains(b)) return;
|
|
137
|
+
const all = btns();
|
|
138
|
+
const i = all.indexOf(b);
|
|
139
|
+
let n = null;
|
|
140
|
+
if (e.key === 'ArrowRight') n = (i + 1) % all.length;
|
|
141
|
+
else if (e.key === 'ArrowLeft') n = (i - 1 + all.length) % all.length;
|
|
142
|
+
else if (e.key === 'Home') n = 0;
|
|
143
|
+
else if (e.key === 'End') n = all.length - 1;
|
|
144
|
+
if (n === null) return;
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
select(all[n], true);
|
|
117
147
|
});
|
|
118
148
|
});
|
|
119
149
|
// Version switcher + account menu — the shared dropdown wiring (open/close +
|
|
@@ -124,7 +154,11 @@ export function wireTopbar(root = document) {
|
|
|
124
154
|
chip.addEventListener('click', () => {
|
|
125
155
|
applyAccent(chip.getAttribute('data-accent-pick'), document.documentElement);
|
|
126
156
|
const group = chip.closest('[data-accent-group]') || root;
|
|
127
|
-
group.querySelectorAll('[data-accent-pick]').forEach((c) =>
|
|
157
|
+
group.querySelectorAll('[data-accent-pick]').forEach((c) => {
|
|
158
|
+
c.classList.toggle('is-active', c === chip);
|
|
159
|
+
// Keep the announced state in step with the visual one.
|
|
160
|
+
if (c.hasAttribute('aria-pressed')) c.setAttribute('aria-pressed', c === chip ? 'true' : 'false');
|
|
161
|
+
});
|
|
128
162
|
});
|
|
129
163
|
});
|
|
130
164
|
// Copy buttons
|
package/src/styles/badge.css
CHANGED
|
@@ -19,28 +19,26 @@
|
|
|
19
19
|
background: var(--surface-2);
|
|
20
20
|
white-space: nowrap;
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
/* Status chips take the --chip-* pairs, so the light app's solid fills live in
|
|
23
|
+
tokens.css rather than in a theme override down here: on white the glow-tints
|
|
24
|
+
wash out and the ink has to deepen to clear AA, and that is a token decision. */
|
|
25
|
+
.ui-badge--live { color: var(--chip-success-ink); background: var(--chip-success-fill); }
|
|
23
26
|
.ui-badge--soon { color: var(--purple-mid); background: var(--glow-purple); }
|
|
24
27
|
.ui-badge--info { color: var(--cyan); background: var(--glow-cyan); }
|
|
25
|
-
.ui-badge--warn { color: var(--
|
|
26
|
-
.ui-badge--danger { color: var(--
|
|
28
|
+
.ui-badge--warn { color: var(--chip-warn-ink); background: var(--chip-warn-fill); }
|
|
29
|
+
.ui-badge--danger { color: var(--chip-danger-ink); background: var(--chip-danger-fill); }
|
|
27
30
|
.ui-badge--archive { color: var(--muted); background: var(--surface); }
|
|
28
31
|
|
|
29
32
|
/* Semantic status variants — for record/row status in data tables
|
|
30
33
|
(paid / verified / pending / failed …). Same chip, mapped to meaning. */
|
|
31
|
-
.ui-badge--success { color: var(--
|
|
32
|
-
.ui-badge--pending { color: var(--
|
|
34
|
+
.ui-badge--success { color: var(--chip-success-ink); background: var(--chip-success-fill); }
|
|
35
|
+
.ui-badge--pending { color: var(--chip-warn-ink); background: var(--chip-warn-fill); }
|
|
33
36
|
.ui-badge--neutral { color: var(--muted); background: var(--surface-2); }
|
|
34
37
|
|
|
35
|
-
/*
|
|
36
|
-
|
|
37
|
-
:root[data-theme="light"] .ui-badge--success,
|
|
38
|
-
:root[data-theme="light"] .ui-badge--live { color: #1f7a38; background: #dff3e4; }
|
|
39
|
-
:root[data-theme="light"] .ui-badge--pending,
|
|
40
|
-
:root[data-theme="light"] .ui-badge--warn { color: #8a5e00; background: #fbedd2; }
|
|
41
|
-
:root[data-theme="light"] .ui-badge--danger { color: #b7295f; background: #fbe0ea; }
|
|
38
|
+
/* The two neutral chips are the exception: their light fill is already in the
|
|
39
|
+
surface scale (--surface-3), and their ink is plain --muted. */
|
|
42
40
|
:root[data-theme="light"] .ui-badge--neutral,
|
|
43
|
-
:root[data-theme="light"] .ui-badge--archive { color:
|
|
41
|
+
:root[data-theme="light"] .ui-badge--archive { color: var(--muted); background: var(--surface-3); }
|
|
44
42
|
|
|
45
43
|
/* Pill: same shape, sentence case, a touch larger — a metadata chip */
|
|
46
44
|
.ui-pill {
|
package/src/styles/callout.css
CHANGED
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
|
|
50
50
|
/* status → the paint tokens every style below consumes (--toast-on = the ink
|
|
51
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:
|
|
53
|
-
.ui-toast--danger { --toast-accent: var(--pink); --toast-glow: var(--glow-pink); --toast-on:
|
|
54
|
-
.ui-toast--warn { --toast-accent: var(--amber); --toast-glow: color-mix(in srgb, var(--amber) 14%, transparent); --toast-on:
|
|
55
|
-
.ui-toast--info { --toast-accent: var(--cyan); --toast-glow: var(--glow-cyan); --toast-on:
|
|
52
|
+
.ui-toast--success { --toast-accent: var(--green); --toast-glow: var(--glow-green); --toast-on: var(--signal-contrast); }
|
|
53
|
+
.ui-toast--danger { --toast-accent: var(--pink); --toast-glow: var(--glow-pink); --toast-on: var(--danger-contrast); }
|
|
54
|
+
.ui-toast--warn { --toast-accent: var(--amber); --toast-glow: color-mix(in srgb, var(--amber) 14%, transparent); --toast-on: var(--signal-contrast); }
|
|
55
|
+
.ui-toast--info { --toast-accent: var(--cyan); --toast-glow: var(--glow-cyan); --toast-on: var(--signal-contrast); }
|
|
56
56
|
.ui-toast--neutral { --toast-accent: var(--muted); --toast-glow: var(--surface-2); --toast-on: var(--strong); }
|
|
57
57
|
|
|
58
58
|
/* left status marker (soft + outline; solid is already a full fill) */
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
width: 48px; height: 48px;
|
|
127
127
|
border-radius: 50%;
|
|
128
128
|
background: var(--green);
|
|
129
|
-
color:
|
|
129
|
+
color: var(--signal-contrast);
|
|
130
130
|
display: grid;
|
|
131
131
|
place-items: center;
|
|
132
132
|
margin: 0 auto 14px;
|
package/src/styles/card.css
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
soft shadow to read as a panel. Dark keeps the borderless deck look. */
|
|
17
17
|
:root[data-theme="light"] .ui-card {
|
|
18
18
|
border: 1px solid var(--border);
|
|
19
|
-
box-shadow:
|
|
19
|
+
box-shadow: var(--shadow-card);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/* A table wider than its card scrolls INSIDE the card instead of bleeding past
|
package/src/styles/drawer.css
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Local tokens (surface/shadow/scrim) so the panel + scrim re-theme cleanly:
|
|
9
9
|
* --drawer-surface panel background --drawer-shadow panel elevation
|
|
10
|
-
* --drawer-scrim backdrop colour (
|
|
10
|
+
* --drawer-scrim backdrop colour (--scrim already re-themes per theme)
|
|
11
11
|
* ========================================================================== */
|
|
12
12
|
|
|
13
13
|
.ui-drawer {
|
|
14
14
|
--drawer-surface: var(--surface-2);
|
|
15
15
|
--drawer-shadow: var(--shadow-lg);
|
|
16
|
-
--drawer-scrim:
|
|
16
|
+
--drawer-scrim: var(--scrim);
|
|
17
17
|
--drawer-w: 420px; /* left|right panels: width */
|
|
18
18
|
--drawer-h: 45vh; /* top|bottom panels: height */
|
|
19
19
|
|
|
@@ -22,8 +22,6 @@
|
|
|
22
22
|
z-index: var(--z-overlay);
|
|
23
23
|
pointer-events: none; /* container never blocks; scrim/panel opt back in */
|
|
24
24
|
}
|
|
25
|
-
:root[data-theme="light"] .ui-drawer { --drawer-scrim: rgba(20, 22, 40, 0.38); }
|
|
26
|
-
|
|
27
25
|
/* Scrim — fades in; visibility trails the fade so the panel can animate out. */
|
|
28
26
|
.ui-drawer__scrim {
|
|
29
27
|
position: absolute;
|
package/src/styles/dropdown.css
CHANGED
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
/* Disabled + danger rows */
|
|
109
109
|
.ui-dropdown__item.is-disabled { opacity: 0.45; cursor: not-allowed; pointer-events: none; }
|
|
110
110
|
.ui-dropdown__item.is-danger .ui-dropdown__label { color: var(--muted); }
|
|
111
|
-
.ui-dropdown__item.is-danger:hover .ui-dropdown__label { color: var(--
|
|
111
|
+
.ui-dropdown__item.is-danger:hover .ui-dropdown__label { color: var(--pink); }
|
|
112
112
|
|
|
113
113
|
/* Grouped sections */
|
|
114
114
|
.ui-dropdown__section + .ui-dropdown__section { border-top: 1px solid var(--border); margin-top: 5px; padding-top: 5px; }
|
package/src/styles/empty.css
CHANGED
package/src/styles/feedback.css
CHANGED
|
@@ -24,17 +24,23 @@
|
|
|
24
24
|
background: var(--ui-fb-pill-grad); color: var(--accent-contrast);
|
|
25
25
|
font-family: var(--font-sans); font-size: 13px; font-weight: var(--weight-semibold); letter-spacing: .01em;
|
|
26
26
|
padding: 9px 17px 9px 15px; border-radius: 999px; cursor: pointer; white-space: nowrap; overflow: hidden;
|
|
27
|
-
box-shadow: 0 10px 26px var(--ui-fb-pill-glow),
|
|
27
|
+
box-shadow: 0 10px 26px var(--ui-fb-pill-glow),
|
|
28
|
+
0 2px 6px color-mix(in srgb, var(--shadow-ink) 28%, transparent),
|
|
29
|
+
inset 0 1px 0 color-mix(in srgb, var(--sheen) 28%, transparent);
|
|
28
30
|
transition: transform .2s var(--easing-spring, cubic-bezier(.34,1.56,.64,1)), opacity .16s ease, box-shadow .2s ease;
|
|
29
31
|
}
|
|
30
32
|
.ui-fbpill.show { opacity: 1; pointer-events: auto; transform: translate(-50%, -100%) translateY(-9px) scale(1); }
|
|
31
|
-
.ui-fbpill:hover {
|
|
33
|
+
.ui-fbpill:hover {
|
|
34
|
+
box-shadow: 0 14px 34px var(--ui-fb-pill-glow),
|
|
35
|
+
0 3px 9px color-mix(in srgb, var(--shadow-ink) 30%, transparent),
|
|
36
|
+
inset 0 1px 0 color-mix(in srgb, var(--sheen) 34%, transparent);
|
|
37
|
+
}
|
|
32
38
|
.ui-fbpill.show:hover { transform: translate(-50%, -100%) translateY(-12px) scale(1.04); }
|
|
33
39
|
.ui-fbpill:active { transform: translate(-50%, -100%) translateY(-8px) scale(.98); }
|
|
34
|
-
.ui-fbpill svg { width: 16px; height: 16px; color: currentColor; filter: drop-shadow(0 1px 1px
|
|
40
|
+
.ui-fbpill svg { width: 16px; height: 16px; color: currentColor; filter: drop-shadow(0 1px 1px color-mix(in srgb, var(--shadow-ink) 25%, transparent)); }
|
|
35
41
|
.ui-fbpill::before {
|
|
36
42
|
content: ""; position: absolute; top: 0; bottom: 0; left: -60%; width: 45%;
|
|
37
|
-
background: linear-gradient(100deg, transparent,
|
|
43
|
+
background: linear-gradient(100deg, transparent, color-mix(in srgb, var(--sheen) 55%, transparent), transparent); transform: skewX(-18deg);
|
|
38
44
|
}
|
|
39
45
|
.ui-fbpill.show::before { animation: uiFbSheen 1.15s ease .12s 1; }
|
|
40
46
|
.ui-fbpill:hover::before { animation: uiFbSheen .7s ease 1; }
|
|
@@ -42,7 +48,7 @@
|
|
|
42
48
|
|
|
43
49
|
/* Scrim behind the open composer */
|
|
44
50
|
.ui-fbscrim {
|
|
45
|
-
position: fixed; inset: 0; z-index: 70; background:
|
|
51
|
+
position: fixed; inset: 0; z-index: 70; background: var(--scrim);
|
|
46
52
|
opacity: 0; pointer-events: none; transition: opacity .2s ease;
|
|
47
53
|
}
|
|
48
54
|
.ui-fbscrim.show { opacity: 1; pointer-events: auto; }
|
|
@@ -90,7 +96,7 @@
|
|
|
90
96
|
.ui-fbck { width: 88px; height: 88px; overflow: visible; display: block; transform-origin: center; animation: uiFbCkSpring 1s cubic-bezier(.22,1,.3,1) both; }
|
|
91
97
|
.ui-fbck-t { fill: none; stroke: var(--surface-2); stroke-width: 9; stroke-linecap: round; stroke-linejoin: round; }
|
|
92
98
|
.ui-fbck-m { fill: none; stroke: var(--green); stroke-width: 9; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 118; stroke-dashoffset: 118; animation: uiFbCkDraw .6s cubic-bezier(.5,0,.3,1) .1s forwards; }
|
|
93
|
-
.ui-fbck-s { fill: none; stroke:
|
|
99
|
+
.ui-fbck-s { fill: none; stroke: var(--sheen); stroke-width: 9; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 15 400; stroke-dashoffset: 133; filter: drop-shadow(0 0 8px var(--green)) drop-shadow(0 0 15px var(--green)); opacity: 0; animation: uiFbCkComet .6s cubic-bezier(.5,0,.3,1) .1s forwards; }
|
|
94
100
|
.ui-fbc__done h4 { color: var(--strong); font-size: 19px; font-weight: var(--weight-semibold); letter-spacing: -.01em; margin-bottom: 7px; }
|
|
95
101
|
.ui-fbc__done p { color: var(--dim); font-size: 13.5px; line-height: 1.55; max-width: 38ch; margin: 0 auto; }
|
|
96
102
|
.ui-fbc__done p b { color: var(--text); font-weight: var(--weight-semibold); }
|
package/src/styles/input.css
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
font-weight: var(--weight-medium);
|
|
11
11
|
margin-bottom: 8px;
|
|
12
12
|
}
|
|
13
|
+
/* Required marker. Decoration only — the control carries `required`, which is
|
|
14
|
+
what assistive technology reads, so this asterisk is aria-hidden. */
|
|
15
|
+
.ui-field__req { color: var(--pink); margin-left: 4px; }
|
|
13
16
|
.ui-field__hint { color: var(--muted); font-size: var(--text-sm); margin-top: 7px; line-height: 1.5; }
|
|
14
17
|
.ui-field__error { color: var(--pink); font-size: var(--text-sm); margin-top: 7px; display: flex; align-items: center; gap: 6px; }
|
|
15
18
|
|
|
@@ -142,7 +145,7 @@
|
|
|
142
145
|
left: 3px;
|
|
143
146
|
width: calc(var(--sw-h) - 6px);
|
|
144
147
|
height: calc(var(--sw-h) - 6px);
|
|
145
|
-
background:
|
|
148
|
+
background: var(--control-knob);
|
|
146
149
|
border-radius: 50%;
|
|
147
150
|
box-shadow: var(--shadow-sm);
|
|
148
151
|
transition: transform var(--dur-med) var(--ease);
|
package/src/styles/layout.css
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
.ui-side a.on svg { opacity: 1; stroke: var(--accent); }
|
|
29
29
|
.ui-side .ssep { height: 1px; background: var(--border); margin: 8px 12px; }
|
|
30
30
|
.ui-side a.out { color: var(--muted); }
|
|
31
|
-
.ui-side a.out:hover { color: var(--
|
|
31
|
+
.ui-side a.out:hover { color: var(--pink); }
|
|
32
32
|
|
|
33
33
|
/* -- Account shell grid -------------------------------------------------- */
|
|
34
34
|
.ui-shell {
|
package/src/styles/motion.css
CHANGED
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
}
|
|
76
76
|
.m-lift:hover {
|
|
77
77
|
transform: translateY(-3px);
|
|
78
|
-
box-shadow: var(--shadow-md
|
|
78
|
+
box-shadow: var(--shadow-md);
|
|
79
79
|
}
|
|
80
80
|
.m-press {
|
|
81
81
|
transition: transform var(--duration-instant, 80ms) var(--easing-sharp, cubic-bezier(0.4, 0, 0.6, 1));
|
package/src/styles/segmented.css
CHANGED
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
transition: color var(--dur-med) var(--ease), background var(--dur-med) var(--ease);
|
|
25
25
|
}
|
|
26
26
|
.ui-seg button:hover { color: var(--text); }
|
|
27
|
+
/* aria-pressed is what segmented() emits now; aria-selected is kept so markup
|
|
28
|
+
copied from an older release keeps painting its active pill. */
|
|
27
29
|
.ui-seg button.is-active,
|
|
30
|
+
.ui-seg button[aria-pressed="true"],
|
|
28
31
|
.ui-seg button[aria-selected="true"] {
|
|
29
32
|
color: var(--strong);
|
|
30
33
|
background: var(--seg-active-bg);
|
package/src/styles/tabs.css
CHANGED
|
@@ -26,3 +26,6 @@
|
|
|
26
26
|
|
|
27
27
|
.ui-tabs__panels { padding-top: 20px; }
|
|
28
28
|
.ui-tabs__panel[hidden] { display: none; }
|
|
29
|
+
/* The panel is focusable (tabindex="0", see tabs.js) so a keyboard user can
|
|
30
|
+
reach text-only content. Show the ring when they land on it. */
|
|
31
|
+
.ui-tabs__panel:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: 8px; }
|
package/src/styles/topbar.css
CHANGED
|
@@ -161,8 +161,8 @@
|
|
|
161
161
|
width: 32px; height: 32px;
|
|
162
162
|
border-radius: 50%;
|
|
163
163
|
border: 1px solid var(--border);
|
|
164
|
-
background: linear-gradient(145deg,
|
|
165
|
-
color:
|
|
164
|
+
background: linear-gradient(145deg, var(--accent-strong), var(--purple));
|
|
165
|
+
color: var(--accent-contrast);
|
|
166
166
|
font-weight: 600;
|
|
167
167
|
font-size: 12.5px;
|
|
168
168
|
display: grid;
|
|
@@ -199,4 +199,4 @@
|
|
|
199
199
|
.amenu a svg { width: 19px; height: 19px; flex: none; stroke: currentColor; fill: none; stroke-width: 1.8; opacity: 0.85; }
|
|
200
200
|
.amenu .asep { height: 1px; background: var(--border); margin: 5px 0; }
|
|
201
201
|
.amenu a.aout { color: var(--muted); }
|
|
202
|
-
.amenu a.aout:hover { color: var(--
|
|
202
|
+
.amenu a.aout:hover { color: var(--pink); }
|
package/src/tokens/accents.css
CHANGED
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
--purple: #c2400f;
|
|
31
31
|
--purple-light: #d64a12;
|
|
32
32
|
--purple-mid: #b8420f;
|
|
33
|
-
|
|
33
|
+
/* Deepened for WCAG AA on the light ground (#fff): #d64a12 was 4.33:1 as text
|
|
34
|
+
and as the ink-on-fill; #a8370c clears AA at 6.53:1. See issue #96. */
|
|
35
|
+
--accent: #a8370c;
|
|
34
36
|
--accent-strong: var(--accent);
|
|
35
37
|
--accent-contrast: #ffffff;
|
|
36
38
|
--glow-purple: rgba(214, 74, 18, 0.10);
|
|
@@ -82,7 +84,9 @@
|
|
|
82
84
|
--purple: #0a865a;
|
|
83
85
|
--purple-light: #0b9c68;
|
|
84
86
|
--purple-mid: #0a9060;
|
|
85
|
-
|
|
87
|
+
/* Deepened for WCAG AA on the light ground (#fff): #0b9c68 was 3.52:1 as text
|
|
88
|
+
and as the ink-on-fill; #087a52 clears AA at 5.36:1. See issue #96. */
|
|
89
|
+
--accent: #087a52;
|
|
86
90
|
--accent-strong: var(--accent);
|
|
87
91
|
--accent-contrast: #ffffff;
|
|
88
92
|
--glow-purple: rgba(11, 156, 104, 0.10);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* ============================================================================
|
|
2
2
|
* apliteni-ui — BRAND PRIMITIVES (generated, do not edit)
|
|
3
3
|
*
|
|
4
|
-
* Synced from apliteni/design-system@
|
|
4
|
+
* Synced from apliteni/design-system@523de68
|
|
5
5
|
* via: npm run tokens:sync
|
|
6
6
|
*
|
|
7
7
|
* The Apliteni umbrella-brand primitives: colour ramps + motion vocabulary
|