@apliteni/apliteni-ui 0.31.0 → 0.33.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/README.md +33 -34
- package/package.json +1 -1
- package/react/README.md +7 -9
- package/react/dist/index.css +10 -2
- package/react/dist/index.d.ts +25 -1
- package/react/dist/index.js +136 -92
- package/src/components/dropdown.js +77 -30
- package/src/components/footer.js +6 -1
- package/src/components/index.js +2 -2
- package/src/components/nav.js +4 -3
- package/src/components/pagination.js +9 -31
- package/src/components/shell.js +197 -30
- package/src/components/stat.js +75 -0
- package/src/components/success.js +12 -1
- package/src/index.css +2 -0
- package/src/index.js +1 -0
- package/src/inline.js +4 -0
- package/src/styles/button.css +6 -5
- package/src/styles/callout.css +7 -4
- package/src/styles/card.css +12 -19
- package/src/styles/command-palette.css +6 -5
- package/src/styles/confirm.css +10 -19
- package/src/styles/drawer.css +6 -6
- package/src/styles/dropdown.css +36 -20
- package/src/styles/feedback.css +11 -13
- package/src/styles/field-zoom.css +20 -0
- package/src/styles/input.css +17 -2
- package/src/styles/layout.css +208 -57
- package/src/styles/motion.css +3 -3
- package/src/styles/nav.css +88 -10
- package/src/styles/pagination.css +1 -0
- package/src/styles/segmented.css +1 -2
- package/src/styles/stat.css +121 -0
- package/src/styles/success.css +2 -2
- package/src/styles/tooltip.css +3 -4
- package/src/styles/topbar.css +36 -10
- package/src/tokens/accents.css +7 -6
- package/src/tokens/tokens.css +51 -38
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Stat band — a row of key figures, as an HTML string.
|
|
2
|
+
// why: docs/specification.md#stat-bands
|
|
3
|
+
//
|
|
4
|
+
// A band is a <dl>: each figure is a <div> holding its label as a <dt> and its
|
|
5
|
+
// value, change and trend as <dd>s, which is the grouping HTML allows inside a
|
|
6
|
+
// description list and the one a screen reader reads as "term, then its values".
|
|
7
|
+
import { esc } from './index.js';
|
|
8
|
+
import { icon } from '../assets/icons.js';
|
|
9
|
+
|
|
10
|
+
export const STAT_VARIANTS = ['band', 'tiles', 'open'];
|
|
11
|
+
export const STAT_TONES = ['good', 'bad', 'neutral'];
|
|
12
|
+
|
|
13
|
+
const cx = (...a) => a.filter(Boolean).join(' ');
|
|
14
|
+
let seq = 0;
|
|
15
|
+
|
|
16
|
+
// The arrow is read off the sign the caller printed, so the figure and the
|
|
17
|
+
// glyph beside it cannot disagree. U+2212, the en dash and an accounting
|
|
18
|
+
// bracket are negatives too: a formatter that typesets its figures uses them.
|
|
19
|
+
const directionOf = (text) => {
|
|
20
|
+
const t = text.trim();
|
|
21
|
+
return /^[-−–(]/.test(t) ? 'down' : /^\+/.test(t) ? 'up' : 'flat';
|
|
22
|
+
};
|
|
23
|
+
const GLYPH = { up: 'arrowUp', down: 'arrowDown', flat: 'minus' };
|
|
24
|
+
const hasChange = (delta) => delta && delta.value != null && delta.value !== '';
|
|
25
|
+
|
|
26
|
+
const deltaHtml = (delta, basisId) => {
|
|
27
|
+
if (!delta) return '';
|
|
28
|
+
if (!hasChange(delta)) {
|
|
29
|
+
return `<dd class="ui-stat__delta ui-stat__delta--none">${esc(delta.none || 'No earlier figure')}</dd>`;
|
|
30
|
+
}
|
|
31
|
+
const text = String(delta.value);
|
|
32
|
+
const dir = GLYPH[delta.direction] ? delta.direction : directionOf(text);
|
|
33
|
+
const own = delta.basis ? ` <span class="ui-stat__basis">${esc(delta.basis)}</span>` : '';
|
|
34
|
+
const describedby = !delta.basis && basisId ? ` aria-describedby="${basisId}"` : '';
|
|
35
|
+
return `<dd class="ui-stat__delta"${describedby}>${icon(GLYPH[dir])}<span class="ui-stat__change">${esc(text)}</span>${own}</dd>`;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// `tone` says whether the change is good news, and nothing else. It is never
|
|
39
|
+
// inferred from the direction: costs going up and unclassified rows going down
|
|
40
|
+
// are both real, and a band that paints every rise green is editorialising.
|
|
41
|
+
// A figure with no change has no news to colour.
|
|
42
|
+
const figure = ({ label = '', value = '', delta, trend = '' }, tile, basisId) => {
|
|
43
|
+
const tone = hasChange(delta) && STAT_TONES.includes(delta.tone) && delta.tone !== 'neutral' ? delta.tone : '';
|
|
44
|
+
const cls = cx('ui-stat', tone && `ui-stat--${tone}`, tile && 'ui-card ui-card--pad-sm');
|
|
45
|
+
// `trend` is trusted markup — an <svg> the caller drew. The kit sizes and
|
|
46
|
+
// colours it and draws no chart of its own.
|
|
47
|
+
return `<div class="${cls}">`
|
|
48
|
+
+ `<dt class="ui-stat__label">${esc(label)}</dt>`
|
|
49
|
+
+ `<dd class="ui-stat__value">${esc(value)}</dd>`
|
|
50
|
+
+ deltaHtml(delta, basisId)
|
|
51
|
+
+ (trend ? `<dd class="ui-stat__trend">${trend}</dd>` : '')
|
|
52
|
+
+ '</div>';
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// `basis` is the band's caption, said once above the figures: what every change
|
|
56
|
+
// is measured against, or on a band with no changes, what the figures cover. A
|
|
57
|
+
// figure measured against something else carries its own `delta.basis`, printed
|
|
58
|
+
// beside the change.
|
|
59
|
+
//
|
|
60
|
+
// The caption comes before the list, the way a table's <caption> does, in every
|
|
61
|
+
// layout. It is one statement about all the figures, so it is read before them
|
|
62
|
+
// and it sits outside every one of them — under a row of tiles it would read as
|
|
63
|
+
// a note on the last card. why: docs/specification.md#stat-bands
|
|
64
|
+
export function statBand({ stats = [], variant = 'tiles', basis = '', label, id } = {}) {
|
|
65
|
+
const v = STAT_VARIANTS.includes(variant) ? variant : 'tiles';
|
|
66
|
+
const base = id ? esc(id) : `ui-stats-${++seq}`;
|
|
67
|
+
const basisId = basis ? `${base}-basis` : '';
|
|
68
|
+
const cls = cx('ui-stats', `ui-stats--${v}`, v === 'band' && 'ui-card');
|
|
69
|
+
const named = label ? ` role="group" aria-label="${esc(label)}"` : '';
|
|
70
|
+
const items = stats.map((s) => figure(s, v === 'tiles', basisId)).join('');
|
|
71
|
+
return `<div class="${cls}"${named}>`
|
|
72
|
+
+ (basis ? `<p class="ui-stats__basis" id="${basisId}">${esc(basis)}</p>` : '')
|
|
73
|
+
+ `<dl class="ui-stats__list">${items}</dl>`
|
|
74
|
+
+ '</div>';
|
|
75
|
+
}
|
|
@@ -64,6 +64,7 @@ function countdownEl({ seconds = 5, label = 'Redirecting' } = {}) {
|
|
|
64
64
|
// changing one never moves the other.
|
|
65
65
|
export function success({
|
|
66
66
|
layout = 'hero', // 'hero' | 'split' | 'compact'
|
|
67
|
+
level, // heading level of the title; see the note below
|
|
67
68
|
backdrop = 'aurora', // 'aurora' | 'glow' | 'flat'
|
|
68
69
|
eyebrow = '',
|
|
69
70
|
title = 'All done',
|
|
@@ -76,6 +77,16 @@ export function success({
|
|
|
76
77
|
const cls = ['ui-sx', `ui-sx--${layout}`, `ui-sx--bd-${backdrop}`, confetti && 'ui-sx--confetti', className]
|
|
77
78
|
.filter(Boolean).join(' ');
|
|
78
79
|
|
|
80
|
+
// The title's rank follows the layout, because the layout is the question
|
|
81
|
+
// "how much of the screen does this own": hero and split ARE the page a flow
|
|
82
|
+
// lands on, so their title is its h1; compact sits beside other content and
|
|
83
|
+
// takes h2. It was an h3 either way, which left a page whose whole content is
|
|
84
|
+
// a success() with no h1 at all — the fault Guidelines / The page names.
|
|
85
|
+
// A caller who knows better passes `level`. The look is the class's.
|
|
86
|
+
// why: docs/specification.md#the-page
|
|
87
|
+
const rank = [1, 2, 3, 4, 5, 6].includes(Number(level)) ? Number(level) : (layout === 'compact' ? 2 : 1);
|
|
88
|
+
const h = `h${rank}`;
|
|
89
|
+
|
|
79
90
|
const bd = backdrop === 'aurora'
|
|
80
91
|
? `<div class="ui-sx__aurora" aria-hidden="true">
|
|
81
92
|
<span class="ui-sx__glow ui-sx__glow--a"></span>
|
|
@@ -99,7 +110,7 @@ export function success({
|
|
|
99
110
|
<div class="ui-sx__visual">${successCheck()}</div>
|
|
100
111
|
<div class="ui-sx__content">
|
|
101
112
|
${eyebrowEl}
|
|
102
|
-
|
|
113
|
+
<${h} class="ui-sx__title">${esc(title)}</${h}>
|
|
103
114
|
${bodyEl}
|
|
104
115
|
${actionsEl}
|
|
105
116
|
${countEl}
|
package/src/index.css
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
@import "./tokens/accents.css";
|
|
17
17
|
@import "./styles/base.css";
|
|
18
18
|
@import "./styles/reduced-motion.css";
|
|
19
|
+
@import "./styles/field-zoom.css";
|
|
19
20
|
@import "./styles/motion.css";
|
|
20
21
|
@import "./styles/button.css";
|
|
21
22
|
@import "./styles/card.css";
|
|
@@ -32,6 +33,7 @@
|
|
|
32
33
|
@import "./styles/command-palette.css";
|
|
33
34
|
@import "./styles/table.css";
|
|
34
35
|
@import "./styles/pagination.css";
|
|
36
|
+
@import "./styles/stat.css";
|
|
35
37
|
@import "./styles/empty.css";
|
|
36
38
|
@import "./styles/callout.css";
|
|
37
39
|
@import "./styles/code.css";
|
package/src/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export * from './components/toasts.js';
|
|
|
17
17
|
export * from './components/success.js';
|
|
18
18
|
export * from './components/loading.js';
|
|
19
19
|
export * from './components/pagination.js';
|
|
20
|
+
export * from './components/stat.js';
|
|
20
21
|
export * from './assets/icons.js';
|
|
21
22
|
export * from './assets/brand.js';
|
|
22
23
|
export * from './motion.js';
|
package/src/inline.js
CHANGED
|
@@ -29,6 +29,7 @@ export const topbarCss = read('styles/topbar.css');
|
|
|
29
29
|
export const styles = {
|
|
30
30
|
base: baseCss,
|
|
31
31
|
reducedMotion: read('styles/reduced-motion.css'),
|
|
32
|
+
fieldZoom: read('styles/field-zoom.css'),
|
|
32
33
|
motion: read('styles/motion.css'),
|
|
33
34
|
button: read('styles/button.css'),
|
|
34
35
|
card: read('styles/card.css'),
|
|
@@ -45,6 +46,7 @@ export const styles = {
|
|
|
45
46
|
commandPalette: read('styles/command-palette.css'),
|
|
46
47
|
table: read('styles/table.css'),
|
|
47
48
|
pagination: read('styles/pagination.css'),
|
|
49
|
+
stat: read('styles/stat.css'),
|
|
48
50
|
empty: read('styles/empty.css'),
|
|
49
51
|
callout: read('styles/callout.css'),
|
|
50
52
|
code: read('styles/code.css'),
|
|
@@ -61,6 +63,7 @@ export const cssText = [
|
|
|
61
63
|
tokensCss,
|
|
62
64
|
styles.base,
|
|
63
65
|
styles.reducedMotion,
|
|
66
|
+
styles.fieldZoom,
|
|
64
67
|
styles.motion,
|
|
65
68
|
styles.button,
|
|
66
69
|
styles.card,
|
|
@@ -77,6 +80,7 @@ export const cssText = [
|
|
|
77
80
|
styles.commandPalette,
|
|
78
81
|
styles.table,
|
|
79
82
|
styles.pagination,
|
|
83
|
+
styles.stat,
|
|
80
84
|
styles.empty,
|
|
81
85
|
styles.callout,
|
|
82
86
|
styles.code,
|
package/src/styles/button.css
CHANGED
|
@@ -102,11 +102,12 @@
|
|
|
102
102
|
border-color: transparent;
|
|
103
103
|
color: var(--disabled-ink-bare);
|
|
104
104
|
}
|
|
105
|
-
/* With no box, the ink is read on whatever is behind it
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
/* With no box, the ink is read on whatever is behind it, so the ghost takes an ink
|
|
106
|
+
set for the dullest ground: in dark 7.49 / 6.31 / 7.00 / 5.44 on --bg / --surface /
|
|
107
|
+
--surface-2 / --surface-3, in light 5.70 / 6.17 / 5.20 / 5.39. The enabled ghost's
|
|
108
|
+
--dim reads about 1.5 times that in both themes. #273 cut this ink to rescue two
|
|
109
|
+
dark pairs from under #220's floor; #295 re-picked --muted against the elevation
|
|
110
|
+
ladder and lifted both clear, so it is the margin now rather than the rescue.
|
|
110
111
|
|
|
111
112
|
#273 got here the long way. It first gave the ghost the flat disabled box
|
|
112
113
|
instead, on the claim that nothing would get less readable. That was false:
|
package/src/styles/callout.css
CHANGED
|
@@ -110,8 +110,12 @@
|
|
|
110
110
|
.ui-toast__timer { position: absolute; left: 0; bottom: 0; height: 3px; width: 100%; background: var(--toast-accent); opacity: 0.85; transform-origin: left; }
|
|
111
111
|
.ui-toast__timer.is-running { animation: ui-toast-timer var(--toast-dur, 5s) linear forwards; /* motion: ambient — the bar spends the toast's dismiss timer, which the caller sets */ }
|
|
112
112
|
|
|
113
|
-
/* style: soft — tinted surface
|
|
114
|
-
|
|
113
|
+
/* style: soft — tinted surface. The shadow used to be its edge, so it takes the
|
|
114
|
+
status-tinted hairline the outline style carries, at half the weight. #295 */
|
|
115
|
+
.ui-toast--soft {
|
|
116
|
+
background: var(--toast-glow);
|
|
117
|
+
border: 1px solid color-mix(in srgb, var(--toast-accent) 22%, transparent);
|
|
118
|
+
}
|
|
115
119
|
|
|
116
120
|
/* style: solid — the status at fill strength, inked with the pole opposite it.
|
|
117
121
|
The fill and the ink are one pair, taken together from --signal-solid-*: dark
|
|
@@ -124,7 +128,6 @@
|
|
|
124
128
|
--toast-ink: var(--signal-solid-ink);
|
|
125
129
|
background: var(--toast-solid);
|
|
126
130
|
color: var(--toast-ink);
|
|
127
|
-
box-shadow: var(--shadow-lg);
|
|
128
131
|
}
|
|
129
132
|
.ui-toast--solid .ui-toast__title { color: var(--toast-ink); }
|
|
130
133
|
/* The body copy is the quieter of the two, but it stays quiet by dilution the
|
|
@@ -144,7 +147,7 @@
|
|
|
144
147
|
.ui-toast--solid .ui-toast__timer { background: color-mix(in srgb, var(--toast-ink) 55%, transparent); }
|
|
145
148
|
|
|
146
149
|
/* style: outline — elevated card with a status-tinted border */
|
|
147
|
-
.ui-toast--outline { background: var(--bg-elevated);
|
|
150
|
+
.ui-toast--outline { background: var(--bg-elevated); border: 1px solid color-mix(in srgb, var(--toast-accent) 40%, transparent); }
|
|
148
151
|
|
|
149
152
|
/* compact — single line, no body text */
|
|
150
153
|
.ui-toast--compact { align-items: center; padding-top: 10px; padding-bottom: 10px; }
|
package/src/styles/card.css
CHANGED
|
@@ -1,25 +1,18 @@
|
|
|
1
1
|
/* ============================================================================
|
|
2
|
-
* Card — .ui-card (filled surface
|
|
3
|
-
* surface + radius, never outlines)
|
|
2
|
+
* Card — .ui-card (a filled surface one step off the page, edged by a hairline)
|
|
4
3
|
* Rows: .ui-card__row / headers: .ui-card__title / .ui-card__sub
|
|
5
4
|
* ========================================================================== */
|
|
6
5
|
|
|
6
|
+
/* Flat since #284 and edged in both themes since #295: the surface is the step, the
|
|
7
|
+
line is what makes it legible. why: docs/specification.md#elevation */
|
|
7
8
|
.ui-card {
|
|
8
9
|
background: var(--surface);
|
|
10
|
+
border: 1px solid var(--border);
|
|
9
11
|
border-radius: var(--radius-xl);
|
|
10
12
|
padding: var(--space-6) 26px;
|
|
11
13
|
position: relative;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
/* Light theme is an all-white app, so a filled card can't rely on a darker-
|
|
15
|
-
than-page surface the way the dark deck does — a hairline border delineates
|
|
16
|
-
it instead. Flat, no drop shadow (#284). `none` rather than no declaration:
|
|
17
|
-
at (0,3,0) it outranks .ui-card--accent's inset ring, which light never showed. */
|
|
18
|
-
:root[data-theme="light"] .ui-card {
|
|
19
|
-
border: 1px solid var(--border);
|
|
20
|
-
box-shadow: none;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
16
|
/* A table wider than its card scrolls INSIDE the card instead of bleeding past
|
|
24
17
|
the rounded corners / clipping its last column. Clipping to the card's
|
|
25
18
|
border-box also keeps zebra stripes from escaping the radius. */
|
|
@@ -32,10 +25,11 @@
|
|
|
32
25
|
.ui-card--pad-sm { padding: var(--space-5); }
|
|
33
26
|
.ui-card--pad-lg { padding: var(--space-8) var(--space-10); }
|
|
34
27
|
|
|
35
|
-
/* An accent-tinted card, e.g. a highlighted plan / success surface
|
|
28
|
+
/* An accent-tinted card, e.g. a highlighted plan / success surface. The accent edge is
|
|
29
|
+
the card's own hairline re-coloured: an inset ring over it would draw two. #295 */
|
|
36
30
|
.ui-card--accent {
|
|
37
31
|
background: color-mix(in srgb, var(--accent) 9%, var(--surface));
|
|
38
|
-
|
|
32
|
+
border-color: color-mix(in srgb, var(--accent) 22%, transparent);
|
|
39
33
|
}
|
|
40
34
|
.ui-card--live {
|
|
41
35
|
background: color-mix(in srgb, var(--green) 10%, var(--surface));
|
|
@@ -44,16 +38,15 @@
|
|
|
44
38
|
/* Interactive card (link / button semantics) */
|
|
45
39
|
.ui-card--interactive {
|
|
46
40
|
cursor: pointer;
|
|
47
|
-
/* Written as a <button>, the card would take the browser's frame, centred text
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
border: 0;
|
|
41
|
+
/* Written as a <button>, the card would take the browser's frame, centred text and
|
|
42
|
+
`font: 400 13.3333px Arial` — a shorthand, so `font: inherit` and not the family
|
|
43
|
+
longhand. .ui-card states the background and, since #295, the border, so a
|
|
44
|
+
`border: 0` here would only delete the hairline. #251 */
|
|
52
45
|
text-align: left;
|
|
53
46
|
font: inherit;
|
|
54
47
|
transition: transform var(--dur-fast) var(--ease);
|
|
55
48
|
}
|
|
56
|
-
/* The lift alone
|
|
49
|
+
/* The lift alone: the kit casts no shadow anywhere (#295), and cards are flat (#284). */
|
|
57
50
|
.ui-card--interactive:hover { transform: translateY(-2px); }
|
|
58
51
|
|
|
59
52
|
/* card() emits the title as an h2, so the text face is named here rather than
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* why: docs/specification.md#the-command-palette */
|
|
16
16
|
|
|
17
17
|
.ui-cmdk {
|
|
18
|
-
|
|
19
|
-
--cmdk-
|
|
18
|
+
/* The floating step; nothing casts. why: docs/specification.md#elevation */
|
|
19
|
+
--cmdk-surface: var(--bg-elevated);
|
|
20
20
|
--cmdk-scrim: var(--scrim);
|
|
21
21
|
--cmdk-w: 560px;
|
|
22
22
|
--cmdk-top: 12vh;
|
|
@@ -71,7 +71,6 @@
|
|
|
71
71
|
background: var(--cmdk-surface);
|
|
72
72
|
border: 1px solid var(--border);
|
|
73
73
|
border-radius: var(--radius-lg);
|
|
74
|
-
box-shadow: var(--cmdk-shadow);
|
|
75
74
|
overflow: hidden;
|
|
76
75
|
opacity: 0;
|
|
77
76
|
transform: translate(-50%, -8px);
|
|
@@ -80,7 +79,7 @@
|
|
|
80
79
|
transform var(--dur-med) var(--ease);
|
|
81
80
|
}
|
|
82
81
|
.ui-cmdk.is-open .ui-cmdk__panel { opacity: 1; transform: translate(-50%, 0); }
|
|
83
|
-
.ui-cmdk__panel:focus-visible { outline: none; box-shadow: var(--
|
|
82
|
+
.ui-cmdk__panel:focus-visible { outline: none; box-shadow: var(--ring); }
|
|
84
83
|
|
|
85
84
|
/* -- The text box --------------------------------------------------------- */
|
|
86
85
|
|
|
@@ -250,7 +249,9 @@
|
|
|
250
249
|
padding: 2px 5px;
|
|
251
250
|
border: 1px solid var(--border);
|
|
252
251
|
border-radius: var(--radius-xs);
|
|
253
|
-
|
|
252
|
+
/* A key cap reads as raised, so it takes the step above the panel rather than
|
|
253
|
+
the card step below it. why: docs/specification.md#elevation */
|
|
254
|
+
background: var(--surface-3);
|
|
254
255
|
font-family: var(--font-sans);
|
|
255
256
|
font-size: var(--text-xs);
|
|
256
257
|
line-height: 1.5;
|
package/src/styles/confirm.css
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
/* Confirm — the kit's confirmation dialog: a centred panel over a scrim, for the
|
|
2
|
-
* question a destructive action has to ask.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* `visibility` is SWITCHED rather than transitioned: transitioned, it is still
|
|
6
|
-
* `hidden` in the frame the class lands, so openConfirm()'s focus() would have
|
|
7
|
-
* nothing to focus. The root owns the switch; panel and scrim inherit it.
|
|
8
|
-
*
|
|
9
|
-
* Local tokens so the panel and scrim re-theme cleanly:
|
|
10
|
-
* --confirm-surface panel background --confirm-shadow panel elevation
|
|
11
|
-
* --confirm-scrim backdrop colour
|
|
12
|
-
*
|
|
2
|
+
* question a destructive action has to ask. Shares its scrim and focus trap with
|
|
3
|
+
* the drawer (see overlay.js). Local tokens: --confirm-surface, --confirm-scrim.
|
|
13
4
|
* why: docs/specification.md#motion */
|
|
14
5
|
|
|
15
6
|
.ui-confirm {
|
|
16
|
-
|
|
17
|
-
--confirm-
|
|
7
|
+
/* The floating step; nothing casts. why: docs/specification.md#elevation */
|
|
8
|
+
--confirm-surface: var(--bg-elevated);
|
|
18
9
|
--confirm-scrim: var(--scrim);
|
|
19
10
|
--confirm-w: var(--panel-md);
|
|
20
11
|
|
|
@@ -26,10 +17,11 @@
|
|
|
26
17
|
z-index: calc(var(--z-overlay) + 2);
|
|
27
18
|
pointer-events: none; /* container never blocks; scrim/panel opt back in */
|
|
28
19
|
|
|
29
|
-
/*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
20
|
+
/* SWITCHED, not transitioned: transitioned, the panel is still `hidden` in the
|
|
21
|
+
frame the class lands and openConfirm()'s focus() has nothing to focus. Only
|
|
22
|
+
the hide is timed, so the panel is on screen while it fades out. `linear` and
|
|
23
|
+
not var(--ease): visibility is discrete, and a curve leaving [0, 1] flips it
|
|
24
|
+
mid-fade. The root owns the switch; panel and scrim inherit it. */
|
|
33
25
|
visibility: hidden;
|
|
34
26
|
transition: visibility var(--dur-med) linear;
|
|
35
27
|
}
|
|
@@ -72,7 +64,6 @@
|
|
|
72
64
|
background: var(--confirm-surface);
|
|
73
65
|
border: 1px solid var(--border);
|
|
74
66
|
border-radius: var(--radius-lg);
|
|
75
|
-
box-shadow: var(--confirm-shadow);
|
|
76
67
|
opacity: 0;
|
|
77
68
|
transform: translateY(8px);
|
|
78
69
|
transition:
|
|
@@ -80,7 +71,7 @@
|
|
|
80
71
|
transform var(--dur-med) var(--ease);
|
|
81
72
|
}
|
|
82
73
|
.ui-confirm.is-open .ui-confirm__panel { opacity: 1; transform: none; }
|
|
83
|
-
.ui-confirm__panel:focus-visible { outline: none; box-shadow: var(--
|
|
74
|
+
.ui-confirm__panel:focus-visible { outline: none; box-shadow: var(--ring); }
|
|
84
75
|
|
|
85
76
|
/* -- Slots ----------------------------------------------------------------- */
|
|
86
77
|
/* The question and the answers keep their size; the consequence between them is
|
package/src/styles/drawer.css
CHANGED
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
* (reduced-motion.css) makes the slide and the fade instant.
|
|
7
7
|
*
|
|
8
8
|
* Local tokens (surface/shadow/scrim) so the panel + scrim re-theme cleanly:
|
|
9
|
-
* --drawer-surface panel background
|
|
9
|
+
* --drawer-surface panel background
|
|
10
10
|
* --drawer-scrim backdrop colour (--scrim already re-themes per theme)
|
|
11
11
|
* ========================================================================== */
|
|
12
12
|
|
|
13
13
|
.ui-drawer {
|
|
14
|
-
--drawer
|
|
15
|
-
|
|
14
|
+
/* The floating step; the edge is the hairline each --drawer--<edge> rule below draws.
|
|
15
|
+
why: docs/specification.md#elevation */
|
|
16
|
+
--drawer-surface: var(--bg-elevated);
|
|
16
17
|
--drawer-scrim: var(--scrim);
|
|
17
18
|
--drawer-w: var(--panel-md); /* left|right panels: width */
|
|
18
19
|
--drawer-h: 45vh; /* top|bottom panels: height */
|
|
@@ -57,10 +58,9 @@
|
|
|
57
58
|
display: flex;
|
|
58
59
|
flex-direction: column;
|
|
59
60
|
background: var(--drawer-surface);
|
|
60
|
-
box-shadow: var(--drawer-shadow);
|
|
61
61
|
transition: transform var(--dur-med) var(--ease);
|
|
62
62
|
}
|
|
63
|
-
.ui-drawer__panel:focus-visible { outline: none; box-shadow: var(--
|
|
63
|
+
.ui-drawer__panel:focus-visible { outline: none; box-shadow: var(--ring); }
|
|
64
64
|
.ui-drawer.is-open .ui-drawer__panel { transform: none; }
|
|
65
65
|
|
|
66
66
|
/* -- Edge anchoring + slide-in direction ---------------------------------- */
|
|
@@ -149,7 +149,7 @@
|
|
|
149
149
|
background var(--dur-fast) var(--ease),
|
|
150
150
|
border-color var(--dur-fast) var(--ease);
|
|
151
151
|
}
|
|
152
|
-
.ui-drawer__close:hover { color: var(--strong); background: var(--surface); }
|
|
152
|
+
.ui-drawer__close:hover { color: var(--strong); background: var(--surface-3); }
|
|
153
153
|
.ui-drawer__close:focus-visible { outline: none; box-shadow: var(--ring); border-color: var(--accent); }
|
|
154
154
|
.ui-drawer__close svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 2.1; }
|
|
155
155
|
|
package/src/styles/dropdown.css
CHANGED
|
@@ -57,13 +57,18 @@
|
|
|
57
57
|
top: calc(100% + var(--ui-dropdown-gap));
|
|
58
58
|
left: 0;
|
|
59
59
|
min-width: 240px;
|
|
60
|
-
|
|
60
|
+
/* The floating step, not the sunken one — a panel held open over a card cannot be
|
|
61
|
+
painted the colour of an inset. why: docs/specification.md#elevation */
|
|
62
|
+
background: var(--bg-elevated);
|
|
61
63
|
border: 1px solid var(--border);
|
|
62
64
|
border-radius: var(--radius-lg);
|
|
63
65
|
padding: 6px;
|
|
64
|
-
box-shadow: var(--shadow-lg);
|
|
65
66
|
opacity: 0;
|
|
66
67
|
visibility: hidden;
|
|
68
|
+
/* Drawn for the whole of the fade out, so the rows do not vanish mid-fade — and a
|
|
69
|
+
box that is drawn is a box that is hit. The open rules take this back.
|
|
70
|
+
why: docs/specification.md#the-dropdown-panel */
|
|
71
|
+
pointer-events: none;
|
|
67
72
|
transform: translateY(-6px);
|
|
68
73
|
transition:
|
|
69
74
|
opacity var(--dur-med) var(--ease),
|
|
@@ -88,7 +93,13 @@
|
|
|
88
93
|
transform: translateY(6px);
|
|
89
94
|
}
|
|
90
95
|
|
|
91
|
-
.ui-dropdown.open .ui-dropdown__panel { opacity: 1; visibility: visible; transform: translateY(0); }
|
|
96
|
+
.ui-dropdown.open .ui-dropdown__panel { opacity: 1; visibility: visible; pointer-events: auto; transform: translateY(0); }
|
|
97
|
+
/* Open, the panel is visible AT ONCE: `visibility` is off the list, so it is not
|
|
98
|
+
held at `hidden` for the frame a key opens the panel in — the frame the focus
|
|
99
|
+
call happens in. Closing still fades on every property it always did.
|
|
100
|
+
why: docs/specification.md#the-dropdown-panel */
|
|
101
|
+
.ui-dropdown.open .ui-dropdown__panel,
|
|
102
|
+
.ui-dropdown__panel--portal.is-open { transition-property: opacity, transform; }
|
|
92
103
|
|
|
93
104
|
/* Portalled panel — wireDropdown() moves it onto <body>, clear of every
|
|
94
105
|
ancestor's overflow and every ancestor's stacking context. The descendant
|
|
@@ -100,7 +111,7 @@
|
|
|
100
111
|
position: fixed;
|
|
101
112
|
top: auto; right: auto; bottom: auto; left: auto;
|
|
102
113
|
}
|
|
103
|
-
.ui-dropdown__panel--portal.is-open { opacity: 1; visibility: visible; transform: translateY(0); }
|
|
114
|
+
.ui-dropdown__panel--portal.is-open { opacity: 1; visibility: visible; pointer-events: auto; transform: translateY(0); }
|
|
104
115
|
|
|
105
116
|
/* Item row — [icon] [main: label + desc] [badge] [tick]. A row is a <div>, an
|
|
106
117
|
<a href> or a <button>: `is-selected` and __tick below mean a row gets
|
|
@@ -126,8 +137,9 @@
|
|
|
126
137
|
color: var(--text);
|
|
127
138
|
transition: background var(--dur-fast) var(--ease);
|
|
128
139
|
}
|
|
129
|
-
|
|
130
|
-
.ui-dropdown__item:
|
|
140
|
+
/* A row in a raised panel lifts: the step above it, never the card step below. why: docs/specification.md#elevation */
|
|
141
|
+
.ui-dropdown__item:hover { background: var(--surface-3); }
|
|
142
|
+
.ui-dropdown__item:focus-visible { outline: none; background: var(--surface-3); box-shadow: var(--ring); }
|
|
131
143
|
.ui-dropdown__main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
132
144
|
.ui-dropdown__label { font-family: var(--font-sans); font-size: 12.5px; color: var(--strong); font-weight: 600; letter-spacing: 0.01em; }
|
|
133
145
|
.ui-dropdown__desc { font-size: 11px; color: var(--muted); line-height: 1.35; }
|
|
@@ -153,17 +165,26 @@
|
|
|
153
165
|
margin-left: auto;
|
|
154
166
|
flex: none;
|
|
155
167
|
color: var(--muted);
|
|
156
|
-
|
|
168
|
+
/* A chip is the top step. why: docs/specification.md#elevation */
|
|
169
|
+
background: var(--surface-3);
|
|
157
170
|
}
|
|
158
171
|
.ui-dropdown__badge.is-live { color: var(--chip-success-ink); background: var(--chip-success-fill); }
|
|
159
|
-
|
|
172
|
+
/* The accent counter keeps the badge's flat surface and changes only the ink — the shape
|
|
173
|
+
src/styles/nav.css:163 `.ui-nav__item.is-active .ui-nav__badge.is-accent` already holds.
|
|
174
|
+
The accent wash goes on a base surface, never a raised one, and #295 made this panel a
|
|
175
|
+
raised one. why: docs/specification.md#elevation */
|
|
176
|
+
.ui-dropdown__badge.is-accent { color: var(--accent); background: var(--surface-3); }
|
|
160
177
|
|
|
161
178
|
/* Disabled + danger rows */
|
|
162
179
|
.ui-dropdown__item.is-disabled { color: var(--disabled-ink); cursor: not-allowed; pointer-events: none; }
|
|
163
180
|
.ui-dropdown__item.is-disabled .ui-dropdown__label,
|
|
164
181
|
.ui-dropdown__item.is-disabled .ui-dropdown__desc { color: inherit; }
|
|
182
|
+
/* Quiet at rest, --pink on the way to being pressed. Both states, not hover alone:
|
|
183
|
+
the ring says where the reader is, not that this row ends something.
|
|
184
|
+
why: docs/specification.md#a-dropdown-row-is-a-div-a-link-or-a-button */
|
|
165
185
|
.ui-dropdown__item.is-danger .ui-dropdown__label { color: var(--muted); }
|
|
166
|
-
.ui-dropdown__item.is-danger:hover .ui-dropdown__label
|
|
186
|
+
.ui-dropdown__item.is-danger:hover .ui-dropdown__label,
|
|
187
|
+
.ui-dropdown__item.is-danger:focus-visible .ui-dropdown__label { color: var(--pink); }
|
|
167
188
|
|
|
168
189
|
/* Grouped sections. `~` and :not([hidden]), because `+` counts a group a search
|
|
169
190
|
query has hidden and drew the divider above the first group left showing. */
|
|
@@ -193,11 +214,6 @@
|
|
|
193
214
|
variant emits, so the plain dropdown renders as it did.
|
|
194
215
|
why: docs/specification.md#a-dropdown-with-a-search-field */
|
|
195
216
|
.ui-dropdown__panel--search { min-width: 280px; }
|
|
196
|
-
/* Open, the panel is visible at once instead of at the first step of the
|
|
197
|
-
`visibility` transition, because a browser will not focus a field in a box
|
|
198
|
-
that is still `hidden` and opening puts focus in the field. Closing still fades. */
|
|
199
|
-
.ui-dropdown.open .ui-dropdown__panel--search,
|
|
200
|
-
.ui-dropdown__panel--search.is-open { transition-property: opacity, transform; }
|
|
201
217
|
|
|
202
218
|
.ui-dropdown__search { position: relative; display: flex; align-items: center; margin-bottom: 6px; }
|
|
203
219
|
.ui-dropdown__search-ic {
|
|
@@ -209,12 +225,16 @@
|
|
|
209
225
|
}
|
|
210
226
|
/* 16px at 2.4 paints 1.6px, over the 1.5px floor stories/glyph-stroke.test.js holds. */
|
|
211
227
|
.ui-dropdown__search-ic svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2.4; }
|
|
228
|
+
/* 12.5px with a mouse only. On a touch screen the touch-zoom net in
|
|
229
|
+
src/styles/field-zoom.css takes this field, and every other field the kit
|
|
230
|
+
ships, to 16px. why: docs/specification.md#a-field-is-16px-on-a-touch-screen */
|
|
212
231
|
.ui-dropdown__search-input {
|
|
213
232
|
width: 100%;
|
|
214
233
|
font: inherit;
|
|
215
234
|
font-size: 12.5px;
|
|
216
235
|
color: var(--strong);
|
|
217
|
-
|
|
236
|
+
/* A field is the sunken step, the same one .ui-input takes. why: docs/specification.md#elevation */
|
|
237
|
+
background: var(--surface-2);
|
|
218
238
|
border: 1px solid var(--border);
|
|
219
239
|
border-radius: var(--radius-sm);
|
|
220
240
|
padding: 8px 12px 8px 34px;
|
|
@@ -223,10 +243,6 @@
|
|
|
223
243
|
min-height: 38px;
|
|
224
244
|
transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
|
|
225
245
|
}
|
|
226
|
-
/* iOS Safari zooms the page into a focused field under 16px, and opening the
|
|
227
|
-
panel focuses this one. A real 16px, not a scaled one: the zoom reads the
|
|
228
|
-
computed size, and a transform would shrink the ring and border with it. */
|
|
229
|
-
@media (pointer: coarse) { .ui-dropdown__search-input { font-size: 16px; } }
|
|
230
246
|
.ui-dropdown__search-input::placeholder { color: var(--muted); }
|
|
231
247
|
.ui-dropdown__search-input:hover { border-color: var(--border-strong); }
|
|
232
248
|
.ui-dropdown__search-input:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
|
|
@@ -238,7 +254,7 @@
|
|
|
238
254
|
|
|
239
255
|
/* The row Enter would pick. Focus stays in the field, which carries the ring,
|
|
240
256
|
so this row takes the hover fill and a bar rather than a second ring. */
|
|
241
|
-
.ui-dropdown__item.is-active { background: var(--surface); box-shadow: inset 2px 0 0 var(--accent); }
|
|
257
|
+
.ui-dropdown__item.is-active { background: var(--surface-3); box-shadow: inset 2px 0 0 var(--accent); }
|
|
242
258
|
|
|
243
259
|
/* No match — a nudge and no action, per Guidelines / Microcopy. Empty while
|
|
244
260
|
anything matches, so it takes no room. */
|
package/src/styles/feedback.css
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
|
|
10
10
|
:root {
|
|
11
11
|
--ui-fb-pill-grad: linear-gradient(135deg, var(--accent), var(--accent-strong));
|
|
12
|
-
--ui-fb-pill-glow: color-mix(in srgb, var(--accent) 45%, transparent);
|
|
13
12
|
--ui-fb-sel: color-mix(in srgb, var(--accent) 22%, transparent);
|
|
14
13
|
}
|
|
15
14
|
|
|
@@ -34,23 +33,21 @@
|
|
|
34
33
|
border: 0; text-align: center; font: inherit;
|
|
35
34
|
font-family: var(--font-sans); font-size: 13px; font-weight: var(--weight-semibold); letter-spacing: .01em;
|
|
36
35
|
padding: 9px 17px 9px 15px; border-radius: 999px; cursor: pointer; white-space: nowrap; overflow: hidden;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
/* The pill cast two layers of ink and an accent glow until #295, and grew both on
|
|
37
|
+
hover. Nothing casts now, and it takes no hairline either: it is the accent at full
|
|
38
|
+
fill strength on the page, found the way the solid toast is found. What is left is an
|
|
39
|
+
inset sheen — gloss on the surface, not a shadow under it — so the hover is the travel
|
|
40
|
+
alone and the box-shadow it transitioned went with it.
|
|
41
|
+
why: docs/specification.md#elevation */
|
|
42
|
+
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--sheen) 28%, transparent);
|
|
40
43
|
transition:
|
|
41
44
|
transform var(--dur-fast) var(--ease-spring),
|
|
42
|
-
opacity var(--dur-fast) var(--ease)
|
|
43
|
-
box-shadow var(--dur-fast) var(--ease);
|
|
45
|
+
opacity var(--dur-fast) var(--ease);
|
|
44
46
|
}
|
|
45
47
|
.ui-fbpill.show { opacity: 1; pointer-events: auto; transform: translate(-50%, -100%) translateY(-9px) scale(1); }
|
|
46
|
-
.ui-fbpill:hover {
|
|
47
|
-
box-shadow: 0 14px 34px var(--ui-fb-pill-glow),
|
|
48
|
-
0 3px 9px color-mix(in srgb, var(--shadow-ink) 30%, transparent),
|
|
49
|
-
inset 0 1px 0 color-mix(in srgb, var(--sheen) 34%, transparent);
|
|
50
|
-
}
|
|
51
48
|
.ui-fbpill.show:hover { transform: translate(-50%, -100%) translateY(-12px) scale(1.04); }
|
|
52
49
|
.ui-fbpill:active { transform: translate(-50%, -100%) translateY(-8px) scale(.98); }
|
|
53
|
-
.ui-fbpill svg { width: 16px; height: 16px; stroke-width: 2.4; color: currentColor;
|
|
50
|
+
.ui-fbpill svg { width: 16px; height: 16px; stroke-width: 2.4; color: currentColor; }
|
|
54
51
|
.ui-fbpill::before {
|
|
55
52
|
content: ""; position: absolute; top: 0; bottom: 0; left: -60%; width: 45%;
|
|
56
53
|
background: linear-gradient(100deg, transparent, color-mix(in srgb, var(--sheen) 55%, transparent), transparent); transform: skewX(-18deg);
|
|
@@ -70,7 +67,8 @@
|
|
|
70
67
|
.ui-fbcomposer {
|
|
71
68
|
position: fixed; z-index: 71; left: 50%; bottom: 26px; transform: translateX(-50%) translateY(14px);
|
|
72
69
|
width: min(520px, calc(100vw - 32px)); background: var(--bg-elevated); border-radius: 18px;
|
|
73
|
-
|
|
70
|
+
/* The shadow was its only edge until #295; now it takes the hairline. */
|
|
71
|
+
border: 1px solid var(--border); opacity: 0; pointer-events: none;
|
|
74
72
|
transition: opacity var(--dur-med) var(--ease), transform var(--dur-med) var(--ease); overflow: hidden;
|
|
75
73
|
}
|
|
76
74
|
.ui-fbcomposer.show { opacity: 1; pointer-events: auto; transform: translateX(-50%) translateY(0); }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/* apliteni-ui — touch-zoom net. iOS Safari zooms the page into a focused field
|
|
2
|
+
* whose text is under 16px and does not zoom back out, so under (pointer: coarse)
|
|
3
|
+
* every field the kit ships is 16px. Element selectors rather than kit classes,
|
|
4
|
+
* and !important for the reason reduced-motion.css takes it; left out are the
|
|
5
|
+
* controls with nothing to type into, which never zoom.
|
|
6
|
+
*
|
|
7
|
+
* The two costs, argued in the spec below: a 16px scaled back down with a
|
|
8
|
+
* transform still zooms, and this is a flat size rather than a floor, so a host
|
|
9
|
+
* field designed ABOVE 16px is shrunk here. The override recipe is there too.
|
|
10
|
+
*
|
|
11
|
+
* why: docs/specification.md#a-field-is-16px-on-a-touch-screen */
|
|
12
|
+
|
|
13
|
+
@media (pointer: coarse) {
|
|
14
|
+
input:not(:where(
|
|
15
|
+
[type="button"], [type="checkbox"], [type="color"], [type="file"], [type="hidden"],
|
|
16
|
+
[type="image"], [type="radio"], [type="range"], [type="reset"], [type="submit"]
|
|
17
|
+
)),
|
|
18
|
+
select,
|
|
19
|
+
textarea { font-size: 16px !important; }
|
|
20
|
+
}
|