@apliteni/apliteni-ui 0.81.0 → 0.84.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/guidelines/component-choice.md +14 -0
- package/guidelines/dense-tables.md +3 -1
- package/package.json +1 -1
- package/react/dist/index.d.ts +4 -1
- package/react/dist/index.js +12 -8
- package/src/components/stat.js +16 -18
- package/src/styles/card.css +2 -1
- package/src/styles/stat.css +37 -8
- package/src/styles/table.css +65 -0
|
@@ -54,6 +54,20 @@
|
|
|
54
54
|
|
|
55
55
|
**Don't:** Give those same list filters tab roles when they have no separate panels.
|
|
56
56
|
|
|
57
|
+
## Group one category at a time
|
|
58
|
+
|
|
59
|
+
<!-- rule: one-category -->
|
|
60
|
+
|
|
61
|
+
**Rule:** Give views, filters, sorting and actions separate groups and styles.
|
|
62
|
+
|
|
63
|
+
**Why:** Identical controls in one bar hide that one of them re-draws every row while its neighbour only narrows them.
|
|
64
|
+
|
|
65
|
+
**Do:** Keep the filter chips in one bar, the views in a strip of their own, and the action at the right-hand end.
|
|
66
|
+
|
|
67
|
+
**Don't:** Put three saved views and a filter control in one bar as four buttons of the same size and style.
|
|
68
|
+
|
|
69
|
+
**Except:** Saved views that re-draw one table take a segmented strip; views that swap panels take tabs.
|
|
70
|
+
|
|
57
71
|
## Match confirmation scale
|
|
58
72
|
|
|
59
73
|
<!-- rule: scale -->
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
|
|
57
57
|
**Rule:** Keep units smaller and in body ink.
|
|
58
58
|
|
|
59
|
-
**Do:** Keep units smaller and in body ink, and explain abbreviations in reachable text.
|
|
59
|
+
**Do:** Keep units smaller and in body ink, and explain abbreviations in reachable text. A short value in a stacked row carries its own unit or count word, because the header that named it is no longer on screen.
|
|
60
60
|
|
|
61
61
|
**Don't:** Hide units in muted text or make readers guess what an abbreviation means.
|
|
62
62
|
|
|
@@ -72,6 +72,8 @@
|
|
|
72
72
|
|
|
73
73
|
**Don't:** Squeeze columns until values are unreadable or silently remove data.
|
|
74
74
|
|
|
75
|
+
**Except:** On a phone, a row whose long column is a paragraph stacks instead of scrolling: the short values on one line, the paragraph under them, and the header kept for screen readers. No column is dropped.
|
|
76
|
+
|
|
75
77
|
## Keep updates stable
|
|
76
78
|
|
|
77
79
|
<!-- rule: states -->
|
package/package.json
CHANGED
package/react/dist/index.d.ts
CHANGED
|
@@ -97,11 +97,14 @@ interface StatDelta {
|
|
|
97
97
|
basis?: string;
|
|
98
98
|
tooltip?: string;
|
|
99
99
|
direction?: 'up' | 'down' | 'flat';
|
|
100
|
-
none?: string;
|
|
101
100
|
}
|
|
102
101
|
interface StatFigure {
|
|
103
102
|
label: string;
|
|
104
103
|
value: ReactNode;
|
|
104
|
+
/** Context that is not a change, such as "of revenue": no arrow, no tone. It
|
|
105
|
+
* takes the row a change would take, and leads that row beside one. A short
|
|
106
|
+
* phrase: the row is one line, and a longer caption is clipped. */
|
|
107
|
+
caption?: string;
|
|
105
108
|
delta?: StatDelta;
|
|
106
109
|
trend?: ReactNode;
|
|
107
110
|
}
|
package/react/dist/index.js
CHANGED
|
@@ -379,17 +379,21 @@ var directionOf = (text2) => {
|
|
|
379
379
|
return /^[-−–(]/.test(t) ? "down" : /^\+/.test(t) ? "up" : "flat";
|
|
380
380
|
};
|
|
381
381
|
var hasChange = (d) => !!d && d.value != null && d.value !== "";
|
|
382
|
-
function
|
|
383
|
-
if (!hasChange(delta)) {
|
|
384
|
-
|
|
385
|
-
|
|
382
|
+
function ContextRow({ caption, delta, basisId }) {
|
|
383
|
+
if (!hasChange(delta)) return caption ? /* @__PURE__ */ jsx11("dd", { className: "ui-stat__caption", children: caption }) : null;
|
|
384
|
+
const lead = caption ? /* @__PURE__ */ jsxs7(Fragment, { children: [
|
|
385
|
+
/* @__PURE__ */ jsx11("span", { className: "ui-stat__caption", children: caption }),
|
|
386
|
+
" "
|
|
387
|
+
] }) : null;
|
|
386
388
|
const dir = delta.direction && GLYPH[delta.direction] ? delta.direction : directionOf(delta.value);
|
|
387
|
-
|
|
389
|
+
const own = delta.basis || "";
|
|
390
|
+
return /* @__PURE__ */ jsxs7("dd", { className: "ui-stat__delta", "aria-describedby": !own && basisId ? basisId : void 0, children: [
|
|
391
|
+
lead,
|
|
388
392
|
/* @__PURE__ */ jsx11(Icon, { name: GLYPH[dir] }),
|
|
389
393
|
delta.tooltip ? /* @__PURE__ */ jsx11(Tooltip, { text: delta.tooltip, children: /* @__PURE__ */ jsx11("span", { className: "ui-stat__change", children: delta.value }) }) : /* @__PURE__ */ jsx11("span", { className: "ui-stat__change", children: delta.value }),
|
|
390
|
-
|
|
394
|
+
own ? /* @__PURE__ */ jsxs7(Fragment, { children: [
|
|
391
395
|
" ",
|
|
392
|
-
/* @__PURE__ */ jsx11("span", { className: "ui-stat__basis", children:
|
|
396
|
+
/* @__PURE__ */ jsx11("span", { className: "ui-stat__basis", children: own })
|
|
393
397
|
] }) : null
|
|
394
398
|
] });
|
|
395
399
|
}
|
|
@@ -406,7 +410,7 @@ function StatBand({ stats, variant = "tiles", basis, basisId: sharedBasisId, lab
|
|
|
406
410
|
return /* @__PURE__ */ jsxs7("div", { className: cls, children: [
|
|
407
411
|
/* @__PURE__ */ jsx11("dt", { className: "ui-stat__label", children: s.label }),
|
|
408
412
|
/* @__PURE__ */ jsx11("dd", { className: "ui-stat__value", children: s.value }),
|
|
409
|
-
|
|
413
|
+
/* @__PURE__ */ jsx11(ContextRow, { caption: s.caption, delta: s.delta, basisId }),
|
|
410
414
|
s.trend ? /* @__PURE__ */ jsx11("dd", { className: "ui-stat__trend", children: s.trend }) : null
|
|
411
415
|
] }, `${s.label}-${i}`);
|
|
412
416
|
}) })
|
package/src/components/stat.js
CHANGED
|
@@ -23,23 +23,26 @@ const directionOf = (text) => {
|
|
|
23
23
|
const GLYPH = { up: 'arrowUp', down: 'arrowDown', flat: 'minus' };
|
|
24
24
|
const hasChange = (delta) => delta && delta.value != null && delta.value !== '';
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
// One row and one line: the caption, the change, then what it is measured
|
|
27
|
+
// against. Nothing to compare draws no row. why: docs/specification.md#stat-bands
|
|
28
|
+
const contextRow = (caption, delta, basisId) => {
|
|
29
|
+
if (!hasChange(delta)) return caption ? `<dd class="ui-stat__caption">${esc(caption)}</dd>` : '';
|
|
30
|
+
// The space is read where the CSS gap is only drawn: "of income+1.2 pts" else.
|
|
31
|
+
const lead = caption ? `<span class="ui-stat__caption">${esc(caption)}</span> ` : '';
|
|
31
32
|
const text = String(delta.value);
|
|
32
33
|
const dir = GLYPH[delta.direction] ? delta.direction : directionOf(text);
|
|
34
|
+
// Never dropped: a basis is passed when the band's caption does not cover this figure.
|
|
33
35
|
const own = delta.basis ? ` <span class="ui-stat__basis">${esc(delta.basis)}</span>` : '';
|
|
34
|
-
const describedby = !
|
|
35
|
-
return `<dd class="ui-stat__delta"${describedby}>${icon(GLYPH[dir])}
|
|
36
|
+
const describedby = !own && basisId ? ` aria-describedby="${basisId}"` : '';
|
|
37
|
+
return `<dd class="ui-stat__delta"${describedby}>${lead}${icon(GLYPH[dir])}`
|
|
38
|
+
+ `<span class="ui-stat__change">${esc(text)}</span>${own}</dd>`;
|
|
36
39
|
};
|
|
37
40
|
|
|
38
41
|
// `tone` says whether the change is good news, and nothing else. It is never
|
|
39
42
|
// inferred from the direction: costs going up and unclassified rows going down
|
|
40
43
|
// are both real, and a band that paints every rise green is editorialising.
|
|
41
44
|
// A figure with no change has no news to colour.
|
|
42
|
-
const figure = ({ label = '', value = '', delta, trend = '' }, tile, basisId) => {
|
|
45
|
+
const figure = ({ label = '', value = '', caption = '', delta, trend = '' }, tile, basisId) => {
|
|
43
46
|
const tone = hasChange(delta) && STAT_TONES.includes(delta.tone) && delta.tone !== 'neutral' ? delta.tone : '';
|
|
44
47
|
const cls = cx('ui-stat', tone && `ui-stat--${tone}`, tile && 'ui-card ui-card--pad-sm');
|
|
45
48
|
// `trend` is trusted markup — an <svg> the caller drew. The kit sizes and
|
|
@@ -47,20 +50,15 @@ const figure = ({ label = '', value = '', delta, trend = '' }, tile, basisId) =>
|
|
|
47
50
|
return `<div class="${cls}">`
|
|
48
51
|
+ `<dt class="ui-stat__label">${esc(label)}</dt>`
|
|
49
52
|
+ `<dd class="ui-stat__value">${esc(value)}</dd>`
|
|
50
|
-
+
|
|
53
|
+
+ contextRow(caption, delta, basisId)
|
|
51
54
|
+ (trend ? `<dd class="ui-stat__trend">${trend}</dd>` : '')
|
|
52
55
|
+ '</div>';
|
|
53
56
|
};
|
|
54
57
|
|
|
55
|
-
// `basis` is the band's caption
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
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
|
|
58
|
+
// `basis` is the band's caption: what every change is measured against, said
|
|
59
|
+
// once before the list the way a table's <caption> is, in every layout — under
|
|
60
|
+
// a row of tiles it would read as a note on the last card.
|
|
61
|
+
// why: docs/specification.md#stat-bands
|
|
64
62
|
export function statBand({ stats = [], variant = 'tiles', basis = '', label, id } = {}) {
|
|
65
63
|
const v = STAT_VARIANTS.includes(variant) ? variant : 'tiles';
|
|
66
64
|
const base = id ? esc(id) : `ui-stats-${++seq}`;
|
package/src/styles/card.css
CHANGED
|
@@ -82,7 +82,8 @@
|
|
|
82
82
|
display: flex;
|
|
83
83
|
align-items: center;
|
|
84
84
|
gap: 10px;
|
|
85
|
-
|
|
85
|
+
/* Title to description: the step, not the 5px it was. why: docs/specification.md#spacing-and-rhythm */
|
|
86
|
+
margin: 0 0 var(--space-2);
|
|
86
87
|
}
|
|
87
88
|
.ui-card__sub {
|
|
88
89
|
color: var(--text);
|
package/src/styles/stat.css
CHANGED
|
@@ -49,9 +49,25 @@
|
|
|
49
49
|
white-space: nowrap;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/* A figure's own words about its value, in the caption rank's size and weight
|
|
53
|
+
with the band's tighter leading, as the label takes, so it claims no rank.
|
|
54
|
+
Body ink on no fill; no tone reaches it, having no change to colour. */
|
|
55
|
+
.ui-stat__caption {
|
|
56
|
+
color: var(--text);
|
|
57
|
+
font-size: var(--text-sm);
|
|
58
|
+
font-weight: var(--weight-normal);
|
|
59
|
+
line-height: var(--leading-snug);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Alone it is the row a change would have taken — the same step down and line
|
|
63
|
+
box — which holds a band of unlike figures on one line. Leading a change it is
|
|
64
|
+
a span in that row, spaced off what follows. */
|
|
65
|
+
dd.ui-stat__caption { margin-top: var(--space-2); }
|
|
66
|
+
.ui-stat__delta .ui-stat__caption { margin-inline-end: var(--space-1); }
|
|
67
|
+
|
|
52
68
|
.ui-stat__delta {
|
|
53
69
|
display: flex;
|
|
54
|
-
flex-wrap:
|
|
70
|
+
flex-wrap: nowrap;
|
|
55
71
|
align-items: center;
|
|
56
72
|
gap: 2px var(--space-1);
|
|
57
73
|
margin-top: var(--space-2);
|
|
@@ -61,9 +77,21 @@
|
|
|
61
77
|
line-height: var(--leading-snug);
|
|
62
78
|
}
|
|
63
79
|
.ui-stat__delta svg { width: 14px; height: 14px; flex: none; stroke-width: 2.7; } /* 2.7 of a 24 box drawn at 14px = 1.58 CSS px. docs/specification.md#icons-and-glyphs */
|
|
64
|
-
.ui-stat__change { font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
80
|
+
.ui-stat__change { font-variant-numeric: tabular-nums; white-space: nowrap; flex: none; }
|
|
65
81
|
.ui-stat__basis { color: var(--text); font-weight: var(--weight-normal); }
|
|
66
|
-
|
|
82
|
+
|
|
83
|
+
/* The change above does not wrap: wrapped, its arrow ends one line and its number
|
|
84
|
+
starts the next, and a long caption drops its own change below the ones beside
|
|
85
|
+
it. The arrow and the number cannot give way, so the words clip — the whole
|
|
86
|
+
string stays in the markup. Only in that row: a caption standing alone has
|
|
87
|
+
nothing to orphan, so it takes a second line rather than lose words. */
|
|
88
|
+
.ui-stat__delta .ui-stat__caption,
|
|
89
|
+
.ui-stat__basis { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
90
|
+
|
|
91
|
+
/* A figure is never narrower than its content: without these a long line widens it. */
|
|
92
|
+
dd.ui-stat__caption { contain: inline-size; }
|
|
93
|
+
.ui-stat__delta .ui-stat__caption,
|
|
94
|
+
.ui-stat__basis { width: 0; flex-grow: 1; max-width: max-content; }
|
|
67
95
|
|
|
68
96
|
/* Tone is set by the caller and says good or bad news. Direction is the arrow's
|
|
69
97
|
job, so an unset tone keeps the change neutral whichever way it points. */
|
|
@@ -77,14 +105,15 @@
|
|
|
77
105
|
.ui-stat--good .ui-stat__trend { color: var(--chip-success-ink); }
|
|
78
106
|
.ui-stat--bad .ui-stat__trend { color: var(--chip-danger-ink); }
|
|
79
107
|
|
|
80
|
-
/* The band's caption, above the figures rather than in one:
|
|
81
|
-
|
|
82
|
-
|
|
108
|
+
/* The band's caption, above the figures rather than in one: what every change
|
|
109
|
+
points at. It governs the row, so it takes the caption rank itself and is
|
|
110
|
+
never set under a caption inside one figure, and its margin is below it. */
|
|
83
111
|
.ui-stats__basis {
|
|
84
112
|
margin: 0 0 var(--space-5);
|
|
85
113
|
color: var(--text);
|
|
86
|
-
|
|
87
|
-
|
|
114
|
+
/* rank: caption */
|
|
115
|
+
font-size: var(--text-sm);
|
|
116
|
+
font-weight: var(--weight-normal);
|
|
88
117
|
}
|
|
89
118
|
|
|
90
119
|
.ui-stats--tiles .ui-stats__list { gap: var(--space-4); }
|
package/src/styles/table.css
CHANGED
|
@@ -241,3 +241,68 @@ a.ui-identity:hover { text-decoration: underline; }
|
|
|
241
241
|
.ui-table--pinned { --ui-table-selection-width: var(--space-12); }
|
|
242
242
|
.ui-table--pinned .ui-table__selection { box-sizing: border-box; width: var(--ui-table-selection-width); min-width: var(--ui-table-selection-width); max-width: var(--ui-table-selection-width); padding-inline: 0; text-align: center; }
|
|
243
243
|
.ui-table--pinned .ui-table__selection + .ui-table__identity { left: var(--ui-table-selection-width); }
|
|
244
|
+
|
|
245
|
+
/* ============================================================================
|
|
246
|
+
* Stacked rows at phone width — .ui-table--stack, .ui-table__long
|
|
247
|
+
*
|
|
248
|
+
* The cell rules name both .ui-table and the modifier on purpose: --dense, --compact and
|
|
249
|
+
* __title reach a cell with two classes, and one class would lose to them and leave
|
|
250
|
+
* `nowrap` on a paragraph.
|
|
251
|
+
*
|
|
252
|
+
* The consumer's half — the roles a changed `display` drops — is in the specification.
|
|
253
|
+
* why: docs/specification.md#dense-financial-tables
|
|
254
|
+
* held by: src/styles/table-stack.test.js (layout), stories/table-stack.test.js (roles)
|
|
255
|
+
* ========================================================================== */
|
|
256
|
+
|
|
257
|
+
/* The marked column holds running text at any width. What the modifier adds below the
|
|
258
|
+
phone step is the line of its own. */
|
|
259
|
+
.ui-table td.ui-table__long { white-space: normal; }
|
|
260
|
+
|
|
261
|
+
@media (max-width: 560px) {
|
|
262
|
+
.ui-table.ui-table--stack { display: block; width: 100%; min-width: 0; padding: 0; }
|
|
263
|
+
.ui-table.ui-table--stack > thead {
|
|
264
|
+
position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%);
|
|
265
|
+
}
|
|
266
|
+
.ui-table.ui-table--stack > tbody { display: block; }
|
|
267
|
+
.ui-table.ui-table--stack > tbody > tr {
|
|
268
|
+
display: flex; flex-wrap: wrap; align-items: baseline;
|
|
269
|
+
column-gap: var(--space-3); row-gap: var(--space-1);
|
|
270
|
+
/* The inset is on the row, not the table: the hover outline draws at the row's own
|
|
271
|
+
edge, so a pad on the table would put the outline back against the text (#71). It is
|
|
272
|
+
the step a dense cell already pays sideways, so the outline and the row separator keep
|
|
273
|
+
the clearance they had at the width above this one. */
|
|
274
|
+
padding: var(--space-3); border-bottom: 1px solid var(--border);
|
|
275
|
+
}
|
|
276
|
+
.ui-table.ui-table--stack > tbody > tr:last-child { border-bottom: 0; }
|
|
277
|
+
/* `height` is in this list for the same reason `white-space` is: --compact sets a row
|
|
278
|
+
height for a one-line cell, and a block cell takes it as a fixed height rather than a
|
|
279
|
+
floor, so a wrapped paragraph spills through the separator into the next row. A stacked
|
|
280
|
+
cell is as tall as its text. */
|
|
281
|
+
.ui-table.ui-table--stack > tbody > tr > td {
|
|
282
|
+
display: block; width: auto; min-width: 0; max-width: 100%; height: auto;
|
|
283
|
+
padding: 0; border-bottom: 0; white-space: normal; overflow-wrap: break-word;
|
|
284
|
+
}
|
|
285
|
+
.ui-table.ui-table--stack > tbody > tr > td.ui-table__long { flex: 0 0 100%; text-align: left; }
|
|
286
|
+
/* A stacked row has no columns left to pin, stick or inset, and the identity's name is
|
|
287
|
+
what the row is about — the fold hides it only because a pinned column is narrow. */
|
|
288
|
+
.ui-table.ui-table--stack.ui-table--pinned > :where(thead, tbody) > tr > :where(.ui-table__identity, .ui-table__selection) {
|
|
289
|
+
position: static; width: auto; min-width: 0; max-width: none;
|
|
290
|
+
padding-inline: 0; border-right: 0; text-align: left;
|
|
291
|
+
}
|
|
292
|
+
.ui-table.ui-table--stack.ui-table--pinned .ui-identity__name {
|
|
293
|
+
position: static; width: auto; height: auto; max-width: none;
|
|
294
|
+
overflow: visible; clip-path: none;
|
|
295
|
+
}
|
|
296
|
+
/* The bleed now pays for the row's inset instead of the end cells', so the cells still
|
|
297
|
+
start on the card's own text edge and the outline keeps its room on both sides. Same
|
|
298
|
+
step, so the arithmetic is the one the sheet already does for a dense table above; these
|
|
299
|
+
rules exist because a stacked table that is not dense gets no bleed from that one. A
|
|
300
|
+
scroll wrapper owns the bleed when it is the card's child, and owes one step more than
|
|
301
|
+
the table does because the wrapper keeps its own --space-1 of focus clearance. */
|
|
302
|
+
.ui-card > .ui-table--stack {
|
|
303
|
+
width: calc(100% + 2 * var(--space-3)); margin-inline: calc(-1 * var(--space-3));
|
|
304
|
+
}
|
|
305
|
+
.ui-card > .ui-table-scroll:has(> .ui-table--stack) {
|
|
306
|
+
margin-inline: calc(-1 * var(--space-4));
|
|
307
|
+
}
|
|
308
|
+
}
|