@apliteni/apliteni-ui 0.91.0 → 0.93.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 +5 -7
- package/package.json +1 -1
- package/react/README.md +2 -2
- package/react/dist/index.d.ts +3 -3
- package/react/dist/index.js +79 -111
- package/src/assets/icons.js +1 -1
- package/src/components/account-nav.js +2 -2
- package/src/components/back.js +2 -2
- package/src/components/command-palette.js +2 -2
- package/src/components/drawer.js +1 -1
- package/src/components/dropdown.js +12 -12
- package/src/components/filter-bar.js +2 -2
- package/src/components/footer.js +1 -1
- package/src/components/index.js +1 -1
- package/src/components/loading.js +1 -1
- package/src/components/shell.js +19 -19
- package/src/components/stat.js +3 -3
- package/src/components/success.js +3 -3
- package/src/components/tooltip.js +1 -1
- package/src/components/topbar.js +1 -1
- package/src/html.js +1 -1
- package/src/index.css +1 -1
- package/src/index.d.ts +1 -1
- package/src/logic/accents.js +1 -1
- package/src/logic/filter-bar.js +4 -4
- package/src/styles/back.css +3 -3
- package/src/styles/base.css +3 -3
- package/src/styles/button.css +3 -3
- package/src/styles/callout.css +2 -2
- package/src/styles/card.css +8 -8
- package/src/styles/command-palette.css +14 -14
- package/src/styles/confirm.css +7 -7
- package/src/styles/drawer.css +8 -8
- package/src/styles/dropdown.css +21 -21
- package/src/styles/feedback.css +2 -2
- package/src/styles/field-zoom.css +1 -1
- package/src/styles/file-drop.css +1 -1
- package/src/styles/filter-bar.css +6 -6
- package/src/styles/footer.css +1 -1
- package/src/styles/input.css +6 -6
- package/src/styles/layout.css +32 -32
- package/src/styles/loading.css +1 -1
- package/src/styles/motion.css +1 -1
- package/src/styles/nav.css +4 -4
- package/src/styles/pagination.css +2 -2
- package/src/styles/reduced-motion.css +2 -2
- package/src/styles/segmented.css +3 -3
- package/src/styles/stat.css +3 -3
- package/src/styles/success.css +1 -1
- package/src/styles/table.css +6 -6
- package/src/styles/tap-zone.css +1 -1
- package/src/styles/tooltip.css +3 -3
- package/src/styles/topbar.css +8 -8
- package/src/tokens/accents.css +1 -1
- package/src/tokens/tokens.css +25 -25
|
@@ -80,7 +80,7 @@ function ddBody({ items, sections }, listbox, sx) {
|
|
|
80
80
|
// The match is a substring of the label, anywhere in it, ignoring case and
|
|
81
81
|
// accents; rows keep their order. The factory and the wiring both ask
|
|
82
82
|
// dropdownMatch(), so a preset query and a typed one hide the same rows.
|
|
83
|
-
// why: docs/
|
|
83
|
+
// why: docs/components.md#a-dropdown-with-a-search-field
|
|
84
84
|
// NFD takes the mark off é or ö; ł, ø, đ and the rest are letters of their own
|
|
85
85
|
// with nothing to take off, so they are mapped by hand.
|
|
86
86
|
const ddIsRow = (it) => it && it !== '---' && !it.separator;
|
|
@@ -207,7 +207,7 @@ export function dropdown({
|
|
|
207
207
|
|
|
208
208
|
// The block the sheet bleeds to the panel's bottom edge. It sits OUTSIDE the
|
|
209
209
|
// unwrapped `footer`, because the block that bleeds is the one that has to
|
|
210
|
-
// touch the edge. why: docs/
|
|
210
|
+
// touch the edge. why: docs/components.md#the-dropdown-panel
|
|
211
211
|
const footBlock = foot ? `<div class="ui-dropdown__foot">${foot}</div>` : '';
|
|
212
212
|
|
|
213
213
|
const ddAttrs = 'data-dropdown'
|
|
@@ -236,7 +236,7 @@ export function dropdown({
|
|
|
236
236
|
// Every wired container, so the close handlers can reach a dropdown wherever it
|
|
237
237
|
// was drawn: `document.querySelectorAll` enters no shadow root and sees no other
|
|
238
238
|
// document. A Set and not a WeakSet, because this has to be walked.
|
|
239
|
-
// why: docs/
|
|
239
|
+
// why: docs/components.md#the-dropdown-panel
|
|
240
240
|
const _ddAll = new Set();
|
|
241
241
|
// One pair of close handlers per document that holds a dropdown, the same way
|
|
242
242
|
// wireShell() listens once per document it is handed.
|
|
@@ -276,7 +276,7 @@ function ddItemsOf(dd) {
|
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
// ---- Search wiring ---------------------------------------------------------
|
|
279
|
-
// why: docs/
|
|
279
|
+
// why: docs/components.md#a-dropdown-with-a-search-field
|
|
280
280
|
const ddSearchOf = (dd) => ddPanelOf(dd)?.querySelector('[data-dd-search]') || null;
|
|
281
281
|
const ddActiveOf = (dd) => ddPanelOf(dd)?.querySelector('[data-dd-item].is-active') || null;
|
|
282
282
|
const ddComposing = (e) => e.isComposing || e.keyCode === 229;
|
|
@@ -342,7 +342,7 @@ export const DD_MENU_FLOOR = 240;
|
|
|
342
342
|
* A panel is anchored at one edge of its dropdown — its inline end when it
|
|
343
343
|
* carries `is-end` — so room is measured from that edge and the slide goes the
|
|
344
344
|
* other way. Measuring an end-anchored panel forwards put one off the page.
|
|
345
|
-
* why: docs/
|
|
345
|
+
* why: docs/components.md#a-filter-row-holds-its-panels
|
|
346
346
|
*
|
|
347
347
|
* @param {Element} dd a `.ui-dropdown` that may be inside a filter row
|
|
348
348
|
* @param {number} [floor] the width to reach for
|
|
@@ -405,7 +405,7 @@ function ddFitFilterPanel(dd, panel) {
|
|
|
405
405
|
* the old width too, and inline beats the sheet, so it is read again off the
|
|
406
406
|
* newly bounded box; only when the fit moved, because re-reading it is itself a
|
|
407
407
|
* write.
|
|
408
|
-
* why: docs/
|
|
408
|
+
* why: docs/components.md#a-filter-row-holds-its-panels */
|
|
409
409
|
function ddRefitFilterPanel(dd, panel) {
|
|
410
410
|
const fit = panel?.style ? filterPanelFit(dd) : null;
|
|
411
411
|
if (!fit || !ddWriteFit(panel, fit)) return;
|
|
@@ -424,7 +424,7 @@ function ddRowOf(dd) {
|
|
|
424
424
|
* `position: absolute`, so nothing in it can move a chip along its row. Any
|
|
425
425
|
* panel, not only the one being fitted: two open menus in one row would
|
|
426
426
|
* otherwise answer each other a mutation at a time.
|
|
427
|
-
* why: docs/
|
|
427
|
+
* why: docs/components.md#a-filter-row-holds-its-panels */
|
|
428
428
|
function ddOurs(node) {
|
|
429
429
|
const el = node?.nodeType === 1 ? node : node?.parentElement;
|
|
430
430
|
return !!el?.closest?.('[data-dropdown-panel]');
|
|
@@ -443,7 +443,7 @@ function ddOurs(node) {
|
|
|
443
443
|
* is laid out.
|
|
444
444
|
*
|
|
445
445
|
* Why those two cover the arithmetic, and the one case they do not:
|
|
446
|
-
* why: docs/
|
|
446
|
+
* why: docs/components.md#a-filter-row-holds-its-panels
|
|
447
447
|
*/
|
|
448
448
|
function ddWatchRow(dd, panel) {
|
|
449
449
|
ddUnwatchRow(dd);
|
|
@@ -664,7 +664,7 @@ function selectOption(dd, item) {
|
|
|
664
664
|
// Click-outside, Escape and the repositioning sweep, registered once per document
|
|
665
665
|
// that holds a dropdown — the same shape wireShell()'s listen() has, and for the
|
|
666
666
|
// same reason: a frame is its own document and a listener on the page's never
|
|
667
|
-
// fires there. why: docs/
|
|
667
|
+
// fires there. why: docs/components.md#the-dropdown-panel
|
|
668
668
|
function ddListen(doc) {
|
|
669
669
|
if (!doc || _ddWiredDocs.has(doc)) return;
|
|
670
670
|
_ddWiredDocs.add(doc);
|
|
@@ -683,7 +683,7 @@ function ddListen(doc) {
|
|
|
683
683
|
};
|
|
684
684
|
/* The fallback re-fit, for a view with no ResizeObserver. Not taken as well where
|
|
685
685
|
* ddWatchRow() is watching: a `resize` event is the worse measurement of the two.
|
|
686
|
-
* why: docs/
|
|
686
|
+
* why: docs/components.md#a-filter-row-holds-its-panels */
|
|
687
687
|
const refit = () => {
|
|
688
688
|
for (const dd of ddLive()) {
|
|
689
689
|
if (dd.classList.contains('open') && !dd.__ddPanel && !dd.__ddRowFit) {
|
|
@@ -712,11 +712,11 @@ export function wireDropdown(root = document) {
|
|
|
712
712
|
// Portal: lift the panel onto <body>. An ancestor whose overflow is not
|
|
713
713
|
// `visible` clips it on both axes, and one that is `position: sticky` opens
|
|
714
714
|
// a stacking context whatever z-index the panel carries — the app rail is
|
|
715
|
-
// both at once. why: docs/
|
|
715
|
+
// both at once. why: docs/components.md#the-dropdown-panel
|
|
716
716
|
if (panel && dd.hasAttribute('data-dropdown-portal')) {
|
|
717
717
|
// The tree the trigger is in, not the page's: a panel lifted out of a
|
|
718
718
|
// frame or a shadow root into the top document leaves its stylesheet and
|
|
719
|
-
// its close handler behind. why: docs/
|
|
719
|
+
// its close handler behind. why: docs/components.md#the-dropdown-panel
|
|
720
720
|
const host = ddHostOf(dd);
|
|
721
721
|
sweepOrphanPanels(host);
|
|
722
722
|
panel.setAttribute('data-dropdown-portal', '');
|
|
@@ -9,7 +9,7 @@ export function filterBar({ filters = [], label = 'Filters', clearLabel = 'Clear
|
|
|
9
9
|
+ `<legend class="ui-filter-bar__legend">${esc(filter.label)}</legend>`
|
|
10
10
|
+ dropdown({ items: filterChipItems(filter), variant: 'select',
|
|
11
11
|
// The chip prints one line; the field's name reaches a reader through the
|
|
12
|
-
// trigger's name and the chip's own legend. why: docs/
|
|
12
|
+
// trigger's name and the chip's own legend. why: docs/components.md#dense-financial-tables
|
|
13
13
|
triggerContent: `<span class="${filterChipUnset(filter) ? 'ui-dropdown__value is-placeholder' : 'ui-dropdown__value'}">${esc(filterChipText(filter))}</span>`,
|
|
14
14
|
ariaLabel: filterChipName(filter), open: !!filter.open && !disabled && !busy && !filter.disabled })
|
|
15
15
|
+ `<button type="button" class="ui-filter-bar__remove" data-filter-remove aria-label="${esc(`Remove ${filter.label} filter`)}">×</button></fieldset>`).join('')
|
|
@@ -19,7 +19,7 @@ export function filterBar({ filters = [], label = 'Filters', clearLabel = 'Clear
|
|
|
19
19
|
// rule — a control with no action to offer is not shown. It returns with
|
|
20
20
|
// the first chip, in the bordered skin rather than the ghost one, because a
|
|
21
21
|
// live action beside two chips has to read as live.
|
|
22
|
-
// why: docs/
|
|
22
|
+
// why: docs/components.md#a-filter-row-holds-its-panels
|
|
23
23
|
+ (filters.length
|
|
24
24
|
? `<span data-filter-clear>${button({ label: clearLabel, size: 'sm' })}</span>`
|
|
25
25
|
: '')
|
package/src/components/footer.js
CHANGED
|
@@ -36,7 +36,7 @@ function linkList(links = []) {
|
|
|
36
36
|
// matter, and h2 is the one rank that cannot skip whatever heading came before
|
|
37
37
|
// it. It was an h4 — the only h4 the kit drew — which read h2 → h4 on the
|
|
38
38
|
// landing page and left a rank a reader hears missing. The look is the class's,
|
|
39
|
-
// not the tag's. why:
|
|
39
|
+
// not the tag's. why: guidelines/the-page.md#the-page
|
|
40
40
|
function column({ title, links = [] } = {}) {
|
|
41
41
|
return `<div class="ui-footer__col">` +
|
|
42
42
|
(title ? `<h2 class="ui-footer__col-title">${esc(title)}</h2>` : '') +
|
package/src/components/index.js
CHANGED
|
@@ -61,7 +61,7 @@ export function statusDot(live = false) {
|
|
|
61
61
|
|
|
62
62
|
// ---- Card ----------------------------------------------------------------
|
|
63
63
|
// The title is a heading one level under the page's h1 unless `level` says
|
|
64
|
-
// otherwise. why: docs/
|
|
64
|
+
// otherwise. why: docs/foundations.md#labels-and-titles
|
|
65
65
|
export function card({ title, sub, body = '', variant, pad, icon: ic, level = 2 } = {}) {
|
|
66
66
|
const cls = cx('ui-card', variant && `ui-card--${variant}`, pad && `ui-card--pad-${pad}`);
|
|
67
67
|
const h = [2, 3, 4, 5, 6].includes(Number(level)) ? `h${Number(level)}` : 'h2';
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Three things land in that body: skeleton() while it fetches, your markup once
|
|
6
6
|
// the rows arrive, deniedState() when the answer came back 403.
|
|
7
7
|
//
|
|
8
|
-
// why: docs/
|
|
8
|
+
// why: docs/components.md#pending-and-denied-states
|
|
9
9
|
import { icon } from '../assets/icons.js';
|
|
10
10
|
import { button, esc } from './index.js';
|
|
11
11
|
import { playEntrance } from '../motion.js';
|
package/src/components/shell.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The kit's one page shell: a full-height rail beside one <main>, in two layouts.
|
|
2
2
|
// `topbar` switches the product band on; it is off unless the caller hands one over.
|
|
3
3
|
// wireShell() once after mounting wires the fold, the nav's groups and the reader's menu.
|
|
4
|
-
// why: docs/
|
|
4
|
+
// why: docs/components.md#the-page-shell
|
|
5
5
|
import { topbar as productTopbar } from './topbar.js';
|
|
6
6
|
import { esc, icon } from './index.js';
|
|
7
7
|
import { sidebarNav, breadcrumbs, wireNav } from './nav.js';
|
|
@@ -12,7 +12,7 @@ import { ACCOUNT_NAV, toMenuTuple, initials } from './account-nav.js';
|
|
|
12
12
|
import { paletteHotkey } from './command-palette.js';
|
|
13
13
|
import { safeUrl } from '../html.js';
|
|
14
14
|
// The one account navigation definition lives in account-nav.js because topbar.js needs
|
|
15
|
-
// it too; re-exported here so the name docs/
|
|
15
|
+
// it too; re-exported here so the name docs/components.md publishes keeps working.
|
|
16
16
|
export { ACCOUNT_NAV };
|
|
17
17
|
|
|
18
18
|
const str = (v) => (v == null ? '' : String(v));
|
|
@@ -44,7 +44,7 @@ const toCrumbs = (crumbs) => (Array.isArray(crumbs) ? crumbs : [])
|
|
|
44
44
|
|
|
45
45
|
// A back link replaces the trail rather than joining it: the two would name the same parent
|
|
46
46
|
// twice. Anything but a record is no back link; its fields go through as given, so a back
|
|
47
|
-
// backLink() refuses leaves the trail standing. why: docs/
|
|
47
|
+
// backLink() refuses leaves the trail standing. why: docs/components.md#the-back-link
|
|
48
48
|
const toBack = (b) => (isRecord(b) && !Array.isArray(b) ? { href: b.href, label: b.label } : null);
|
|
49
49
|
|
|
50
50
|
// The reader, as two strings. railUser() and initials() both read them, and an
|
|
@@ -83,15 +83,15 @@ const toTopbar = (t) => {
|
|
|
83
83
|
//
|
|
84
84
|
// Each is a name the kit knows or the default — read strictly, and not through
|
|
85
85
|
// String(), which turned `['topbar']` into a layout and drew half a page.
|
|
86
|
-
// why: docs/
|
|
86
|
+
// why: docs/components.md#the-page-shell
|
|
87
87
|
const toLayout = (v) => (v === 'topbar' ? 'topbar' : 'rail');
|
|
88
88
|
|
|
89
|
-
// why: docs/
|
|
89
|
+
// why: docs/foundations.md#widths
|
|
90
90
|
const toWidth = (v) => (v === 'wide' ? 'wide' : 'centered');
|
|
91
91
|
|
|
92
92
|
// `search: 'palette-id'`, or the same id in `{ palette }` with a placeholder beside
|
|
93
93
|
// it. No palette to open, no field — the argument `signOutHref` takes.
|
|
94
|
-
// why: docs/
|
|
94
|
+
// why: docs/components.md#the-page-shell
|
|
95
95
|
const toSearch = (v) => {
|
|
96
96
|
const given = typeof v === 'string' ? { palette: v } : (isRecord(v) && !Array.isArray(v) ? v : null);
|
|
97
97
|
const palette = given && typeof given.palette === 'string' ? given.palette.trim() : '';
|
|
@@ -104,7 +104,7 @@ const toSearch = (v) => {
|
|
|
104
104
|
// attribute, falling through to --measure. It must REMOVE the property rather than pass
|
|
105
105
|
// a bad value on: a custom property accepts any token stream, so garbage is a valid
|
|
106
106
|
// declaration that drops the column to `none`, the full track.
|
|
107
|
-
// why: docs/
|
|
107
|
+
// why: docs/foundations.md#widths
|
|
108
108
|
const LENGTH = /^(?:\d+|\d*\.\d+)(?:px|rem|em|ch|%|vw)$/;
|
|
109
109
|
const mainMax = (v) => {
|
|
110
110
|
const s = str(v).trim();
|
|
@@ -114,7 +114,7 @@ const mainMax = (v) => {
|
|
|
114
114
|
// ---- the fold, and where the reader's choice is kept ---------------------
|
|
115
115
|
//
|
|
116
116
|
// A cookie rather than localStorage, because a server can read one and paint the
|
|
117
|
-
// rail at the width the reader left it. why: docs/
|
|
117
|
+
// rail at the width the reader left it. why: docs/components.md#the-page-shell
|
|
118
118
|
export const RAIL_COOKIE = 'apliteni-ui-rail';
|
|
119
119
|
const RAIL_MAX_AGE = 60 * 60 * 24 * 365;
|
|
120
120
|
const RAIL_VALUE = new RegExp(`(?:^|;\\s*)${RAIL_COOKIE}=(collapsed|expanded)(?:;|$)`);
|
|
@@ -142,7 +142,7 @@ const railName = (collapsed) => (collapsed ? 'Expand sidebar' : 'Collapse sideba
|
|
|
142
142
|
// hand. The seam does not move: it IS the rail, and the compartment it cuts off
|
|
143
143
|
// stays on the side the rail is on. Only the nodes are written here — a child a
|
|
144
144
|
// stylesheet can reach — and they are spliced into icon()'s own wrapper rather
|
|
145
|
-
// than a copy of it. why: docs/
|
|
145
|
+
// than a copy of it. why: docs/components.md#the-page-shell
|
|
146
146
|
const MARK = '<rect x="3" y="3" width="18" height="18" rx="2"/>'
|
|
147
147
|
+ '<path class="ui-app__fold-seam" d="M9 3v18"/>'
|
|
148
148
|
+ '<path class="ui-app__fold-arrow" d="m16 15-3-3 3-3"/>';
|
|
@@ -151,7 +151,7 @@ const railMark = () => icon('').replace('></svg>', `>${MARK}</svg>`);
|
|
|
151
151
|
// The rail's own skin, outside the <nav>: folding the rail is not a place to go.
|
|
152
152
|
// It stands at the far end of the head band's brand row, and its name is written
|
|
153
153
|
// out rather than put in a tooltip, because the name IS the chip layout.css lands
|
|
154
|
-
// beside the glyph — at both widths. why: docs/
|
|
154
|
+
// beside the glyph — at both widths. why: docs/components.md#the-page-shell
|
|
155
155
|
const railToggle = (collapsed) =>
|
|
156
156
|
`<div class="ui-app__fold-row">`
|
|
157
157
|
+ `<button type="button" class="ui-nav__item ui-app__fold" data-rail-toggle`
|
|
@@ -165,7 +165,7 @@ const SHAPES = {
|
|
|
165
165
|
nav: toItems, crumbs: toCrumbs, back: toBack, account: toReader, maxWidth: mainMax, topbar: toTopbar,
|
|
166
166
|
layout: toLayout, width: toWidth, search: toSearch,
|
|
167
167
|
// Drawn by default; `collapsible: false` is the way out, for a page that will
|
|
168
|
-
// never call wireShell(). why: docs/
|
|
168
|
+
// never call wireShell(). why: docs/components.md#the-page-shell
|
|
169
169
|
collapsible: (v) => v !== false,
|
|
170
170
|
// A boolean is the caller's answer. Anything else leaves it to the reader.
|
|
171
171
|
collapsed: (v) => (typeof v === 'boolean' ? v : null),
|
|
@@ -186,7 +186,7 @@ function settle(options) {
|
|
|
186
186
|
// The face of the reader block: the initials, and the two lines beside them that
|
|
187
187
|
// the fold takes away. `named` is the accessible name when nothing else carries
|
|
188
188
|
// one; under the menu trigger it is null, because the button is named by the words
|
|
189
|
-
// inside it. why: docs/
|
|
189
|
+
// inside it. why: docs/components.md#the-page-shell
|
|
190
190
|
const readerFace = (name, email, named, markOnly = false) =>
|
|
191
191
|
`<span class="ui-app__av"${named ? ` role="img" aria-label="Signed in as ${esc(named)}"` : ' aria-hidden="true"'}>`
|
|
192
192
|
+ `${esc(initials(name, email))}</span>`
|
|
@@ -235,7 +235,7 @@ function readerBlock({ name, email }, signOutHref, { band = false } = {}) {
|
|
|
235
235
|
|
|
236
236
|
// A field to look at, a button to press. `data-cmdk-open` and the cap's class are the
|
|
237
237
|
// palette's own, and the cap is NOT aria-hidden: it is half the button's name.
|
|
238
|
-
// why: docs/
|
|
238
|
+
// why: docs/components.md#the-page-shell
|
|
239
239
|
const searchField = ({ palette, placeholder }) =>
|
|
240
240
|
`<button type="button" class="ui-app__search" data-cmdk-open="${esc(palette)}" aria-haspopup="dialog">`
|
|
241
241
|
+ `<span class="ui-app__search-ic" aria-hidden="true">${icon('search')}</span>`
|
|
@@ -244,7 +244,7 @@ const searchField = ({ palette, placeholder }) =>
|
|
|
244
244
|
+ `</button>`;
|
|
245
245
|
|
|
246
246
|
// The band: search at its start, the reader at its end, beside the rail rather than
|
|
247
|
-
// across the top of both. why: docs/
|
|
247
|
+
// across the top of both. why: docs/components.md#the-page-shell
|
|
248
248
|
const shellBar = (search, account, signOutHref) =>
|
|
249
249
|
`<header class="ui-app__bar">`
|
|
250
250
|
+ (search ? searchField(search) : '')
|
|
@@ -292,13 +292,13 @@ export function appShell(options = {}) {
|
|
|
292
292
|
ariaLabel: navLabel,
|
|
293
293
|
});
|
|
294
294
|
// One band over a page, never two: the banded layout draws its own, so the
|
|
295
|
-
// compatibility bag is not drawn beside it. why: docs/
|
|
295
|
+
// compatibility bag is not drawn beside it. why: docs/components.md#the-page-shell
|
|
296
296
|
const compat = banded ? null : topbar;
|
|
297
297
|
// The topbar already says the product word, so the rail head steps aside when there is
|
|
298
298
|
// one. The word is the link's only text and the narrow rail folds it out of view, so
|
|
299
299
|
// the name is written out — the mark itself is aria-hidden. The banded layout's own
|
|
300
300
|
// bar says no word, so there the rail keeps the lockup: that layout's head band IS
|
|
301
|
-
// the product's mark. why: docs/
|
|
301
|
+
// the product's mark. why: docs/components.md#the-page-shell
|
|
302
302
|
const brand = compat ? '' : `<a class="ui-app__brand" href="${esc(safeUrl(brandHref))}" aria-label="${esc(word)}">`
|
|
303
303
|
+ `${prism(`appb-${++_shellUid}`, 24)}<span>${esc(word)}</span></a>`;
|
|
304
304
|
// A <div>, not an <aside>: <aside> is the `complementary` landmark, and this holds the
|
|
@@ -319,7 +319,7 @@ export function appShell(options = {}) {
|
|
|
319
319
|
? `<div class="ui-app__head">${brand}${inHead}</div>`
|
|
320
320
|
: '';
|
|
321
321
|
// The rail's foot: the reader, or the control that took their place. One block
|
|
322
|
-
// either way. why: docs/
|
|
322
|
+
// either way. why: docs/components.md#the-page-shell
|
|
323
323
|
const foot = banded
|
|
324
324
|
? (fold ? `<div class="ui-app__foot">${fold}</div>` : '')
|
|
325
325
|
: readerBlock(account, signOutHref);
|
|
@@ -357,7 +357,7 @@ const _unpersisted = new WeakSet();
|
|
|
357
357
|
// path is written once — the listener, the reflector and wireShell() all have to mean
|
|
358
358
|
// the same control. A descendant and not a child, because the two layouts stand the
|
|
359
359
|
// row in two places: the head band, or the rail's foot.
|
|
360
|
-
// why: docs/
|
|
360
|
+
// why: docs/components.md#the-page-shell
|
|
361
361
|
const FOLD_PATH = '.ui-app__fold-row > [data-rail-toggle]';
|
|
362
362
|
const RAIL_FOLD = `.ui-app__rail ${FOLD_PATH}`;
|
|
363
363
|
|
|
@@ -413,7 +413,7 @@ export function wireShell(root = document, { persist } = {}) {
|
|
|
413
413
|
// other. Idempotent, and a shell with no account draws none to find.
|
|
414
414
|
wireDropdown(root);
|
|
415
415
|
// A server cannot know which key the reader holds, so the browser corrects it here —
|
|
416
|
-
// off the ROOT's window, not the global one. why: docs/
|
|
416
|
+
// off the ROOT's window, not the global one. why: docs/components.md#the-page-shell
|
|
417
417
|
const here = root.nodeType === 9 ? root : root.ownerDocument;
|
|
418
418
|
const view = here && here.defaultView;
|
|
419
419
|
const key = paletteHotkey(view && view.navigator ? view.navigator.platform : '');
|
package/src/components/stat.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Stat band — a row of key figures, as an HTML string.
|
|
2
|
-
// why: docs/
|
|
2
|
+
// why: docs/components.md#stat-bands
|
|
3
3
|
//
|
|
4
4
|
// A band is a <dl>: each figure is a <div> holding its label as a <dt> and its
|
|
5
5
|
// value, change and trend as <dd>s, which is the grouping HTML allows inside a
|
|
@@ -24,7 +24,7 @@ const GLYPH = { up: 'arrowUp', down: 'arrowDown', flat: 'minus' };
|
|
|
24
24
|
const hasChange = (delta) => delta && delta.value != null && delta.value !== '';
|
|
25
25
|
|
|
26
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/
|
|
27
|
+
// against. Nothing to compare draws no row. why: docs/components.md#stat-bands
|
|
28
28
|
const contextRow = (caption, delta, basisId) => {
|
|
29
29
|
if (!hasChange(delta)) return caption ? `<dd class="ui-stat__caption">${esc(caption)}</dd>` : '';
|
|
30
30
|
// The space is read where the CSS gap is only drawn: "of income+1.2 pts" else.
|
|
@@ -58,7 +58,7 @@ const figure = ({ label = '', value = '', caption = '', delta, trend = '' }, til
|
|
|
58
58
|
// `basis` is the band's caption: what every change is measured against, said
|
|
59
59
|
// once before the list the way a table's <caption> is, in every layout — under
|
|
60
60
|
// a row of tiles it would read as a note on the last card.
|
|
61
|
-
// why: docs/
|
|
61
|
+
// why: docs/components.md#stat-bands
|
|
62
62
|
export function statBand({ stats = [], variant = 'tiles', basis = '', label, id } = {}) {
|
|
63
63
|
const v = STAT_VARIANTS.includes(variant) ? variant : 'tiles';
|
|
64
64
|
const base = id ? esc(id) : `ui-stats-${++seq}`;
|
|
@@ -10,7 +10,7 @@ import { esc, button } from './index.js';
|
|
|
10
10
|
|
|
11
11
|
// 'line' is the bare Lucide `check`, 'circled' is Lucide circle-check-big. Which
|
|
12
12
|
// is the default, and why that departs from Guidelines / Iconography:
|
|
13
|
-
// why: docs/
|
|
13
|
+
// why: docs/components.md#success-confirmations
|
|
14
14
|
// Each is written whole because the icon-sizing and glyph-stroke gates find their
|
|
15
15
|
// subjects by scanning source for a class on an `<svg …>`: a tag split across
|
|
16
16
|
// string pieces leaves both silent rather than failing.
|
|
@@ -66,7 +66,7 @@ function countdownEl({ seconds = 5, label = 'Redirecting' } = {}) {
|
|
|
66
66
|
// Both carry one title and at most one short line under it. There is no eyebrow
|
|
67
67
|
// tier: a confirmation stacking a label, a headline and a paragraph reads as
|
|
68
68
|
// three competing voices for one outcome.
|
|
69
|
-
// why: docs/
|
|
69
|
+
// why: docs/components.md#success-confirmations
|
|
70
70
|
export function success({
|
|
71
71
|
layout = 'hero', // 'hero' | 'split' | 'compact'
|
|
72
72
|
level, // heading level of the title; see the note below
|
|
@@ -88,7 +88,7 @@ export function success({
|
|
|
88
88
|
// takes h2. It was an h3 either way, which left a page whose whole content is
|
|
89
89
|
// a success() with no h1 at all — the fault Guidelines / The page names.
|
|
90
90
|
// A caller who knows better passes `level`. The look is the class's.
|
|
91
|
-
// why:
|
|
91
|
+
// why: guidelines/the-page.md#the-page
|
|
92
92
|
const rank = [1, 2, 3, 4, 5, 6].includes(Number(level)) ? Number(level) : (layout === 'compact' ? 2 : 1);
|
|
93
93
|
const h = `h${rank}`;
|
|
94
94
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// A mark carries `data-tip-value`, with `data-tip-label` and `data-tip-detail`;
|
|
12
12
|
// a `[data-tip-anchor]` inside it is where the readout opens. Under a finger
|
|
13
13
|
// there is no hover, so a tap is the switch: see Pointer kinds below.
|
|
14
|
-
// why: docs/
|
|
14
|
+
// why: docs/components.md#the-hover-readout
|
|
15
15
|
import { esc } from './index.js';
|
|
16
16
|
|
|
17
17
|
const cx = (...a) => a.filter(Boolean).join(' ');
|
package/src/components/topbar.js
CHANGED
|
@@ -30,7 +30,7 @@ export function deckTextSwitch(active = 'deck') {
|
|
|
30
30
|
// bespoke .vsw/.vopt classes for a pixel-identical look, but emits the generic
|
|
31
31
|
// [data-dropdown] hooks so there's ONE open/close/keyboard implementation.
|
|
32
32
|
// `badge` is a tone key and the kit writes the word for it; the stylesheet used
|
|
33
|
-
// to uppercase the key itself. why: docs/
|
|
33
|
+
// to uppercase the key itself. why: docs/foundations.md#labels-and-titles
|
|
34
34
|
const VBADGE = { live: 'Live', archive: 'Archive' };
|
|
35
35
|
export function versionSwitcher(versions = [], activeIdx = 0) {
|
|
36
36
|
const cur = versions[activeIdx]?.label || '';
|
package/src/html.js
CHANGED
|
@@ -5,7 +5,7 @@ export const esc = value => String(value == null ? '' : value).replace(/[&<>"]/g
|
|
|
5
5
|
// Legacy topbar/brand slots accept already escaped HTML. Preserve their entities.
|
|
6
6
|
export const trustedAttr = value => String(value ?? '').replace(/[<>"]/g, c => ENTITIES[c]);
|
|
7
7
|
|
|
8
|
-
// why: docs/
|
|
8
|
+
// why: docs/components.md#escaping-and-url-slots
|
|
9
9
|
export function safeUrl(value, fallback = '#') {
|
|
10
10
|
const raw = String(value ?? '');
|
|
11
11
|
const scheme = raw.replace(/[\t\n\r]/g, '').replace(/^[\u0000-\u0020]+/, '');
|
package/src/index.css
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* (--font-sans, everything else). Weights 300-700 in both. Neither is
|
|
10
10
|
* bundled; a family that never loads falls back to the system stack behind
|
|
11
11
|
* it, in silence.
|
|
12
|
-
* why: docs/
|
|
12
|
+
* why: docs/foundations.md#typefaces
|
|
13
13
|
* ========================================================================== */
|
|
14
14
|
@import "./tokens/brand.generated.css";
|
|
15
15
|
@import "./tokens/tokens.css";
|
package/src/index.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export declare const DD_MENU_FLOOR: number;
|
|
|
41
41
|
* the width it reached for, and whether it is anchored at its inline end.
|
|
42
42
|
* `null` unless the dropdown is inside a `.ui-filter-bar__chip`: the slide is a
|
|
43
43
|
* chip's offset along its row, so a panel anchored to the row itself is not this
|
|
44
|
-
* function's subject and sizes itself. why: docs/
|
|
44
|
+
* function's subject and sizes itself. why: docs/components.md */
|
|
45
45
|
export declare function filterPanelFit(
|
|
46
46
|
dd: Element | null | undefined,
|
|
47
47
|
floor?: number,
|
package/src/logic/accents.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// react/src/AccentPicker.tsx call accentSwatchStyle(); site/chrome.mjs and
|
|
6
6
|
// site/index.html still hand-keep the same four gradients. Held by
|
|
7
7
|
// stories/accent-swatch.test.js, which derives both sides out of src/tokens/.
|
|
8
|
-
// why: docs/
|
|
8
|
+
// why: docs/components.md#page-furniture. See issues #190 and #429.
|
|
9
9
|
export const ACCENTS = ['default', 'phoenix', 'ocean', 'emerald'];
|
|
10
10
|
|
|
11
11
|
// Per accent, the gradient's two stops — each the value src/tokens/ declares for
|
package/src/logic/filter-bar.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* `value` is display text: it is the chip's only visible line, so a consumer
|
|
8
8
|
* answers a change with text a reader can read, not a raw code.
|
|
9
|
-
* why: docs/
|
|
9
|
+
* why: docs/components.md#dense-financial-tables
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const text = (v) => (v == null ? '' : String(v));
|
|
@@ -71,7 +71,7 @@ function stopsAround(host) {
|
|
|
71
71
|
* order, else the nearest one behind it. A filter bar that has just dropped its
|
|
72
72
|
* last control keeps no box the focus ring can sit on, so the focus has to
|
|
73
73
|
* leave the bar rather than land on a line with no height.
|
|
74
|
-
* why: docs/
|
|
74
|
+
* why: docs/components.md#a-filter-row-holds-its-panels */
|
|
75
75
|
export function nextFocusStop(host) {
|
|
76
76
|
return stopsAround(host)[0] || null;
|
|
77
77
|
}
|
|
@@ -88,7 +88,7 @@ export function nextFocusStop(host) {
|
|
|
88
88
|
* Being reachable is not the same as taking the focus: a control can be visible
|
|
89
89
|
* and enabled and still refuse, so each candidate is asked and then checked, and
|
|
90
90
|
* the next one tried.
|
|
91
|
-
* why: docs/
|
|
91
|
+
* why: docs/components.md#a-filter-row-holds-its-panels */
|
|
92
92
|
export function focusNextStop(host) {
|
|
93
93
|
const doc = host && host.ownerDocument;
|
|
94
94
|
if (!doc) return null;
|
|
@@ -114,7 +114,7 @@ export function focusNextStop(host) {
|
|
|
114
114
|
* consumer wrote them left whatever they had marked standing — a menu checking
|
|
115
115
|
* `All`, `aria-selected="true"` on it, under a chip printing `Asia`. The
|
|
116
116
|
* consumer's own array and objects are not written to. #550
|
|
117
|
-
* why: docs/
|
|
117
|
+
* why: docs/components.md#a-filter-row-holds-its-panels */
|
|
118
118
|
export function filterChipItems(filter = {}) {
|
|
119
119
|
const items = filter.items || [];
|
|
120
120
|
const chosen = text(filter.value);
|
package/src/styles/back.css
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* The way up from a page to the page it sits under. It stands above the page
|
|
4
4
|
* title, in the slot a breadcrumb trail would take, and it is quiet at rest: it
|
|
5
5
|
* is on every page below a list, and it is never what the page is for.
|
|
6
|
-
* why: docs/
|
|
6
|
+
* why: docs/components.md#the-back-link
|
|
7
7
|
* ========================================================================== */
|
|
8
8
|
|
|
9
9
|
/* Its own line, and only as wide as its words, so the hover ground does not run
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
/* 16px, with the stroke raised to match: the glyph set draws at 1.7 on a 24-unit
|
|
38
38
|
box, which is 1.13 CSS px at this size and under the kit's 1.5 for a stroked
|
|
39
|
-
mark. 2.4 paints 1.6. why: docs/
|
|
39
|
+
mark. 2.4 paints 1.6. why: docs/foundations.md#icons-and-glyphs */
|
|
40
40
|
.ui-back svg { width: 16px; height: 16px; flex: none; stroke-width: 2.4; }
|
|
41
41
|
|
|
42
42
|
/* In the page shell it takes the trail's place, and the trail's distance from
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
.ui-app__main > .ui-back { margin-bottom: var(--space-5); }
|
|
45
45
|
|
|
46
46
|
/* The one part that can run long. It clips rather than wrapping, as .ui-nav__label
|
|
47
|
-
does. why: docs/
|
|
47
|
+
does. why: docs/components.md#the-back-link */
|
|
48
48
|
.ui-back__label {
|
|
49
49
|
min-width: 0;
|
|
50
50
|
overflow: hidden;
|
package/src/styles/base.css
CHANGED
|
@@ -43,7 +43,7 @@ a {
|
|
|
43
43
|
* reader notices. A component that wants a heading tag set in the text face
|
|
44
44
|
* says so on its own rule, which outranks this one at (0,1,0); .ui-card__title,
|
|
45
45
|
* .ui-drawer__title and .ui-confirm__title are the three that do.
|
|
46
|
-
* why: docs/
|
|
46
|
+
* why: docs/foundations.md#typefaces */
|
|
47
47
|
h1, h2, h3, h4, h5, h6 {
|
|
48
48
|
font-family: var(--font-display);
|
|
49
49
|
}
|
|
@@ -52,7 +52,7 @@ h1, h2, h3, h4, h5, h6 {
|
|
|
52
52
|
* chat feeds run at, 700 stops reading as emphasis and starts reading as a
|
|
53
53
|
* filled-in shape. The semibold step is the emphasis; the bold step stays
|
|
54
54
|
* available to anything that asks for it by name.
|
|
55
|
-
* why: docs/
|
|
55
|
+
* why: docs/foundations.md#typefaces */
|
|
56
56
|
b,
|
|
57
57
|
strong {
|
|
58
58
|
font-weight: var(--weight-semibold);
|
|
@@ -115,7 +115,7 @@ strong {
|
|
|
115
115
|
.ui-stack > * + * { margin-top: var(--space-4); }
|
|
116
116
|
|
|
117
117
|
/* Eyebrow — the label above a title or a figure, in sentence case like every
|
|
118
|
-
label in the kit. why: docs/
|
|
118
|
+
label in the kit. why: docs/foundations.md#labels-and-titles */
|
|
119
119
|
.ui-eyebrow {
|
|
120
120
|
/* rank: label */
|
|
121
121
|
font-size: var(--text-sm);
|
package/src/styles/button.css
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
padding: var(--btn-pad-y) var(--btn-pad-x);
|
|
21
21
|
cursor: pointer;
|
|
22
22
|
/* Light needs a stronger line here: --border on this fill measures 1.047:1.
|
|
23
|
-
why: docs/
|
|
23
|
+
why: docs/foundations.md#elevation */
|
|
24
24
|
border: 1px solid var(--control-edge);
|
|
25
25
|
background: var(--surface); /* ring-gap: inherit — control paint; its focus indicator uses the containing surface. */
|
|
26
26
|
color: var(--text);
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/* -- Sizes -------------------------------------------------------------- */
|
|
77
|
-
/* why: docs/
|
|
77
|
+
/* why: docs/components.md#extra-small-buttons */
|
|
78
78
|
.ui-btn--xs { --btn-pad-y: 4.5px; --btn-pad-x: 8px; --btn-font: var(--text-xs); min-width: 24px; min-height: 24px; }
|
|
79
79
|
.ui-btn--xs svg { width: 13px; height: 13px; stroke-width: 2.8; }
|
|
80
80
|
.ui-btn--sm { --btn-pad-y: 6px; --btn-pad-x: 12px; --btn-font: calc(var(--text-sm) - 0.5px); }
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
contrast table shows that. The box was reverted and this ink shipped.
|
|
124
124
|
Held by src/styles/button-disabled.test.js. */
|
|
125
125
|
|
|
126
|
-
/* why: docs/
|
|
126
|
+
/* why: docs/components.md#busy-button-labels */
|
|
127
127
|
.ui-btn { position: relative; }
|
|
128
128
|
.ui-btn[aria-busy="true"] { pointer-events: none; }
|
|
129
129
|
.ui-btn[aria-busy="true"][data-btn-wired] {
|
package/src/styles/callout.css
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
.ui-callout__body { min-width: 0; overflow-wrap: break-word; }
|
|
16
16
|
.ui-callout__actions { margin-top: 15px; }
|
|
17
17
|
.ui-callout__icon { width: 18px; height: 18px; flex: none; margin-top: 1px; /* muted-ink: glyph — nonverbal callout icon */ color: var(--muted); }
|
|
18
|
-
.ui-callout__icon svg { width: 100%; height: 100%; stroke: currentColor; fill: none; stroke-width: 2.1; } /* 2.1 of a 24 box drawn at 18px = 1.58 CSS px, which is what buys the 3:1 graphic bar rather than the 4.5:1 text one. docs/
|
|
18
|
+
.ui-callout__icon svg { width: 100%; height: 100%; stroke: currentColor; fill: none; stroke-width: 2.1; } /* 2.1 of a 24 box drawn at 18px = 1.58 CSS px, which is what buys the 3:1 graphic bar rather than the 4.5:1 text one. docs/foundations.md#icons-and-glyphs */
|
|
19
19
|
.ui-callout b { color: var(--text); font-weight: var(--weight-semibold); }
|
|
20
20
|
.ui-callout code { font-family: var(--font-mono); font-size: 0.92em; color: var(--text); }
|
|
21
21
|
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
the 20px success() draws it at, rather than stretching a status mark to fill the
|
|
134
134
|
line mark's box. The mark fills the box, so the box is what decides. The modifier
|
|
135
135
|
is written by successPanel(), as success() writes ui-sx--check-circled on its root.
|
|
136
|
-
why: docs/
|
|
136
|
+
why: docs/components.md#success-confirmations */
|
|
137
137
|
.ui-success__check--circled { width: 20px; height: 20px; }
|
|
138
138
|
.ui-success__title { color: var(--strong); font-size: var(--text-lg); font-weight: var(--weight-semibold); margin-bottom: 5px; }
|
|
139
139
|
.ui-success__sub { color: var(--text); font-size: var(--text-base); }
|
package/src/styles/card.css
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
/* The surface is the step; what makes it legible depends on the theme. Dark edges the
|
|
7
7
|
card with the hairline (--card-edge). Light draws no line at all and casts --elev-rest
|
|
8
8
|
instead — a 22px penumbra off a 10px offset, the width Artur's reference uses on a
|
|
9
|
-
card. Chosen by him on #490 round t3. why: docs/
|
|
9
|
+
card. Chosen by him on #490 round t3. why: docs/foundations.md#elevation */
|
|
10
10
|
.ui-card {
|
|
11
11
|
background: var(--surface); --ring-gap: var(--surface);
|
|
12
12
|
border: 1px solid var(--card-edge);
|
|
13
13
|
box-shadow: var(--elev-rest);
|
|
14
14
|
border-radius: var(--radius-xl);
|
|
15
15
|
/* Was `var(--space-6) 26px`; 26 is off the scale and broke the column's text edge.
|
|
16
|
-
why: docs/
|
|
16
|
+
why: docs/foundations.md#spacing-and-rhythm */
|
|
17
17
|
padding: var(--space-6);
|
|
18
18
|
position: relative;
|
|
19
19
|
}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
answered with the native outline. It is a scroll region, so it takes --ring-scroll:
|
|
27
27
|
the band drawn inward, inside the card's 24px padding and clear of the table.
|
|
28
28
|
--elev-rest is left alone: the band is an outline, not a layer of the shadow list.
|
|
29
|
-
why: docs/
|
|
29
|
+
why: docs/foundations.md#the-focus-ring */
|
|
30
30
|
.ui-card:has(> .ui-table):focus-visible { outline: var(--ring-scroll); outline-offset: var(--ring-scroll-offset); }
|
|
31
31
|
|
|
32
32
|
/* Vertical stacks space cards with the container's gap — see .ui-card-stack.
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
/* A padding modifier moves the vertical rhythm and keeps the text edge, so cards of
|
|
37
37
|
different padding line up in one column. --pad-lg is the centred landing card, never
|
|
38
38
|
stacked with plain ones, and keeps its own inset.
|
|
39
|
-
why: docs/
|
|
39
|
+
why: docs/foundations.md#spacing-and-rhythm */
|
|
40
40
|
.ui-card--pad-sm { padding: var(--space-5) var(--space-6); }
|
|
41
41
|
.ui-card--pad-lg { padding: var(--space-8) var(--space-10); }
|
|
42
42
|
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
the card's own hairline re-coloured: an inset ring over it would draw two. #295
|
|
45
45
|
The strength is --card-tint, per theme: light mixes down from white, so a tint that
|
|
46
46
|
works in dark sinks a light card below the page. Each variant edges itself in its own
|
|
47
|
-
colour. why: docs/
|
|
47
|
+
colour. why: docs/foundations.md#elevation */
|
|
48
48
|
.ui-card--accent {
|
|
49
49
|
background: color-mix(in srgb, var(--accent) var(--card-tint), var(--surface)); --ring-gap: color-mix(in srgb, var(--accent) var(--card-tint), var(--surface));
|
|
50
50
|
border-color: color-mix(in srgb, var(--accent) 22%, transparent);
|
|
@@ -66,12 +66,12 @@
|
|
|
66
66
|
transition: transform var(--dur-fast) var(--ease);
|
|
67
67
|
}
|
|
68
68
|
/* The lift alone: the card's own rung is what says it is raised, and hover is not a
|
|
69
|
-
change of level. why: docs/
|
|
69
|
+
change of level. why: docs/foundations.md#elevation */
|
|
70
70
|
.ui-card--interactive:hover { transform: translateY(-2px); }
|
|
71
71
|
.ui-card--interactive:focus-visible { outline: 2px solid transparent; box-shadow: var(--ring); }
|
|
72
72
|
|
|
73
73
|
/* card() emits the title as an h2, so the text face is named here rather than
|
|
74
|
-
left to the element. why: docs/
|
|
74
|
+
left to the element. why: docs/foundations.md#labels-and-titles */
|
|
75
75
|
.ui-card__title {
|
|
76
76
|
color: var(--strong);
|
|
77
77
|
font-family: var(--font-sans);
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
display: flex;
|
|
83
83
|
align-items: center;
|
|
84
84
|
gap: 10px;
|
|
85
|
-
/* Title to description: the step, not the 5px it was. why: docs/
|
|
85
|
+
/* Title to description: the step, not the 5px it was. why: docs/foundations.md#spacing-and-rhythm */
|
|
86
86
|
margin: 0 0 var(--space-2);
|
|
87
87
|
}
|
|
88
88
|
.ui-card__sub {
|