@apliteni/apliteni-ui 0.23.4 → 0.25.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 +21 -1
- package/package.json +1 -1
- package/src/components/dropdown.js +129 -7
- package/src/index.css +6 -1
- package/src/styles/base.css +22 -0
- package/src/styles/confirm.css +4 -1
- package/src/styles/drawer.css +4 -0
- package/src/styles/dropdown.css +38 -1
- package/src/styles/layout.css +3 -0
- package/src/styles/topbar.css +4 -0
- package/src/tokens/tokens.css +11 -2
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ npm install @apliteni/apliteni-ui
|
|
|
48
48
|
## Use it
|
|
49
49
|
|
|
50
50
|
```js
|
|
51
|
-
import '@apliteni/apliteni-ui/css'; // once, at app root (
|
|
51
|
+
import '@apliteni/apliteni-ui/css'; // once, at app root (load the two fonts too — see below)
|
|
52
52
|
import { button, card, topbar, wireTopbar } from '@apliteni/apliteni-ui';
|
|
53
53
|
|
|
54
54
|
el.innerHTML = topbar({ word: 'Strategy', account: { name, email } })
|
|
@@ -56,6 +56,26 @@ el.innerHTML = topbar({ word: 'Strategy', account: { name, email } })
|
|
|
56
56
|
wireTopbar(document); // theme toggle, menus, segmented, copy buttons
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
### The two fonts
|
|
60
|
+
|
|
61
|
+
The kit names two families and bundles neither, so the host page loads them. Poppins is
|
|
62
|
+
`--font-display` — headings, brand marks, large readouts. IBM Plex Sans is `--font-sans` —
|
|
63
|
+
tables, fields, paragraphs, chat, which is most of an application. Weights 300-700 in both:
|
|
64
|
+
|
|
65
|
+
```html
|
|
66
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
67
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&family=IBM+Plex+Sans:wght@300;400;500;600;700&display=swap">
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Load only one of them and the other falls through to the system stack behind it, without
|
|
71
|
+
saying so. Want the old
|
|
72
|
+
single-family look back? Set both roles to the same family in your own stylesheet, after the
|
|
73
|
+
kit's:
|
|
74
|
+
|
|
75
|
+
```css
|
|
76
|
+
:root { --font-sans: var(--font-display); }
|
|
77
|
+
```
|
|
78
|
+
|
|
59
79
|
### Reuse the account page
|
|
60
80
|
|
|
61
81
|
The whole `/account` layout (topbar + sticky sidebar + page body) ships as one
|
package/package.json
CHANGED
|
@@ -76,6 +76,8 @@ function ddBody({ items, sections }, listbox) {
|
|
|
76
76
|
* @param {string} [o.header] raw HTML pinned to the top of the panel
|
|
77
77
|
* @param {string} [o.footer] raw HTML pinned to the bottom of the panel
|
|
78
78
|
* @param {string} [o.align] 'start' (default) | 'end' — the edge the panel hugs
|
|
79
|
+
* @param {string} [o.direction] 'down' (default) | 'up' | 'auto' — the way it opens
|
|
80
|
+
* @param {boolean} [o.portal] mount the panel on <body>, for a clipping or sticky ancestor
|
|
79
81
|
* @param {boolean|number} [o.scroll] true, or a maxHeight in px, to cap and scroll
|
|
80
82
|
* @param {boolean} [o.open] render already-open (handy for screenshots)
|
|
81
83
|
* @param {string} [o.ariaLabel] accessible name for the panel and trigger
|
|
@@ -84,7 +86,8 @@ function ddBody({ items, sections }, listbox) {
|
|
|
84
86
|
export function dropdown({
|
|
85
87
|
label, value, placeholder = 'Select…', variant, items, sections,
|
|
86
88
|
header = '', footer = '', triggerContent, triggerClass = '', chevron = true,
|
|
87
|
-
align = 'start',
|
|
89
|
+
align = 'start', direction = 'down', portal = false,
|
|
90
|
+
scroll = false, open = false, ariaLabel, id, panelClass = '',
|
|
88
91
|
} = {}) {
|
|
89
92
|
const flat = sections ? sections.flatMap((s) => s.items || []) : (items || []);
|
|
90
93
|
const isSelect = variant === 'select' || (variant == null && flat.some((it) => it && (it.selected || it.value != null)));
|
|
@@ -104,15 +107,30 @@ export function dropdown({
|
|
|
104
107
|
ariaLabel && triggerContent != null ? `aria-label="${esc(ariaLabel)}"` : '',
|
|
105
108
|
].filter(Boolean).join(' ');
|
|
106
109
|
|
|
110
|
+
// `is-open` beside `open` because the descendant selector the panel normally
|
|
111
|
+
// takes its open state from stops matching once wireDropdown() portals it.
|
|
107
112
|
const panelAttrs = [
|
|
108
|
-
`class="${cx(
|
|
113
|
+
`class="${cx(
|
|
114
|
+
'ui-dropdown__panel',
|
|
115
|
+
align === 'end' && 'is-end',
|
|
116
|
+
direction === 'up' && 'is-up',
|
|
117
|
+
scroll && 'is-scroll',
|
|
118
|
+
portal && 'ui-dropdown__panel--portal',
|
|
119
|
+
portal && open && 'is-open',
|
|
120
|
+
panelClass,
|
|
121
|
+
)}"`,
|
|
109
122
|
'data-dropdown-panel',
|
|
110
123
|
`role="${listRole}"`,
|
|
111
124
|
ariaLabel ? `aria-label="${esc(ariaLabel)}"` : '',
|
|
112
125
|
scroll && scroll !== true ? `style="max-height:${typeof scroll === 'number' ? scroll + 'px' : esc(scroll)}"` : '',
|
|
113
126
|
].filter(Boolean).join(' ');
|
|
114
127
|
|
|
115
|
-
|
|
128
|
+
const ddAttrs = 'data-dropdown'
|
|
129
|
+
+ (isSelect ? ' data-dropdown-select' : '')
|
|
130
|
+
+ (direction === 'auto' ? ' data-dropdown-direction="auto"' : '')
|
|
131
|
+
+ (portal ? ' data-dropdown-portal' : '');
|
|
132
|
+
|
|
133
|
+
return `<div class="${cx('ui-dropdown', open && 'open')}" ${ddAttrs}${id ? ` id="${esc(id)}"` : ''}>` +
|
|
116
134
|
`<button ${triggerAttrs}>${trig}${chevron ? '<span class="ui-dropdown__chevron" aria-hidden="true"></span>' : ''}</button>` +
|
|
117
135
|
`<div ${panelAttrs}>${header}${ddBody({ items, sections }, isSelect)}${footer}</div>` +
|
|
118
136
|
`</div>`;
|
|
@@ -129,16 +147,74 @@ export function dropdown({
|
|
|
129
147
|
// document. Safe to call repeatedly (e.g. Storybook re-renders).
|
|
130
148
|
let _ddGlobalWired = false;
|
|
131
149
|
|
|
150
|
+
// The trigger-to-panel offset is --ui-dropdown-gap in src/styles/dropdown.css.
|
|
151
|
+
// This is the fallback for a document that has not loaded the sheet;
|
|
152
|
+
// src/components/dropdown.test.js pins the two to each other.
|
|
153
|
+
const DD_GAP = 9;
|
|
154
|
+
|
|
155
|
+
// A portalled panel is no longer a descendant of its container, so everything
|
|
156
|
+
// below asks the container for its panel rather than querying inside it.
|
|
157
|
+
const ddPanelOf = (dd) => dd.__ddPanel || dd.querySelector('[data-dropdown-panel]');
|
|
158
|
+
|
|
159
|
+
function ddGap(panel) {
|
|
160
|
+
const declared = parseFloat(getComputedStyle(panel).getPropertyValue('--ui-dropdown-gap'));
|
|
161
|
+
return Number.isFinite(declared) ? declared : DD_GAP;
|
|
162
|
+
}
|
|
163
|
+
|
|
132
164
|
function ddItemsOf(dd) {
|
|
133
|
-
const panel = dd
|
|
165
|
+
const panel = ddPanelOf(dd);
|
|
134
166
|
if (!panel) return [];
|
|
135
167
|
return Array.from(panel.querySelectorAll('[data-dd-item]'))
|
|
136
168
|
.filter((el) => el.getAttribute('aria-disabled') !== 'true');
|
|
137
169
|
}
|
|
138
170
|
|
|
171
|
+
// `auto` is the only direction the wiring decides; `up` and the default are the
|
|
172
|
+
// panel's own class, set once at render. Flip only when below is too tight AND
|
|
173
|
+
// above is roomier, so a panel that fits nowhere still opens the way it says.
|
|
174
|
+
function ddResolveDirection(dd, panel) {
|
|
175
|
+
if (dd.getAttribute('data-dropdown-direction') !== 'auto') return;
|
|
176
|
+
const trigger = dd.querySelector('[data-dropdown-trigger]');
|
|
177
|
+
if (!trigger || typeof trigger.getBoundingClientRect !== 'function') return;
|
|
178
|
+
const t = trigger.getBoundingClientRect();
|
|
179
|
+
const below = window.innerHeight - t.bottom;
|
|
180
|
+
panel.classList.toggle('is-up', below < panel.offsetHeight + ddGap(panel) && t.top > below);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Nothing lays a portalled panel out any more, so these four inline values are
|
|
184
|
+
// its layout. Inline, so no rule in any sheet can pin the opposite edge.
|
|
185
|
+
function positionPortalPanel(dd, panel) {
|
|
186
|
+
const trigger = dd.querySelector('[data-dropdown-trigger]');
|
|
187
|
+
if (!trigger || typeof trigger.getBoundingClientRect !== 'function') return;
|
|
188
|
+
const t = trigger.getBoundingClientRect();
|
|
189
|
+
const gap = ddGap(panel);
|
|
190
|
+
const s = panel.style;
|
|
191
|
+
if (panel.classList.contains('is-up')) {
|
|
192
|
+
s.top = 'auto';
|
|
193
|
+
s.bottom = `${window.innerHeight - t.top + gap}px`;
|
|
194
|
+
} else {
|
|
195
|
+
s.bottom = 'auto';
|
|
196
|
+
s.top = `${t.bottom + gap}px`;
|
|
197
|
+
}
|
|
198
|
+
if (panel.classList.contains('is-end')) {
|
|
199
|
+
s.left = 'auto';
|
|
200
|
+
s.right = `${window.innerWidth - t.right}px`;
|
|
201
|
+
} else {
|
|
202
|
+
s.right = 'auto';
|
|
203
|
+
s.left = `${t.left}px`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// A panel left on <body> outlives the container that owned it — a re-render
|
|
208
|
+
// replaces the container and the old panel has nothing pointing at it.
|
|
209
|
+
function sweepOrphanPanels() {
|
|
210
|
+
document.querySelectorAll('body > [data-dropdown-panel][data-dropdown-portal]')
|
|
211
|
+
.forEach((p) => { if (!p.__ddOwner || !p.__ddOwner.isConnected) p.remove(); });
|
|
212
|
+
}
|
|
213
|
+
|
|
139
214
|
function closeDropdown(dd) {
|
|
140
215
|
if (!dd.classList.contains('open')) return;
|
|
141
216
|
dd.classList.remove('open');
|
|
217
|
+
ddPanelOf(dd)?.classList.remove('is-open');
|
|
142
218
|
dd.querySelector('[data-dropdown-trigger]')?.setAttribute('aria-expanded', 'false');
|
|
143
219
|
}
|
|
144
220
|
|
|
@@ -148,6 +224,11 @@ function closeAllDropdowns(except) {
|
|
|
148
224
|
|
|
149
225
|
function openDropdown(dd, focusIdx) {
|
|
150
226
|
closeAllDropdowns(dd);
|
|
227
|
+
const panel = ddPanelOf(dd);
|
|
228
|
+
if (panel) {
|
|
229
|
+
ddResolveDirection(dd, panel);
|
|
230
|
+
if (dd.__ddPanel) { positionPortalPanel(dd, panel); panel.classList.add('is-open'); }
|
|
231
|
+
}
|
|
151
232
|
dd.classList.add('open');
|
|
152
233
|
dd.querySelector('[data-dropdown-trigger]')?.setAttribute('aria-expanded', 'true');
|
|
153
234
|
if (focusIdx != null) {
|
|
@@ -161,7 +242,7 @@ function openDropdown(dd, focusIdx) {
|
|
|
161
242
|
function selectOption(dd, item) {
|
|
162
243
|
if (!dd.hasAttribute('data-dropdown-select')) return;
|
|
163
244
|
ddItemsOf(dd).forEach((el) => el.setAttribute('aria-selected', el === item ? 'true' : 'false'));
|
|
164
|
-
dd
|
|
245
|
+
ddPanelOf(dd)?.querySelectorAll('[data-dd-item].is-selected').forEach((el) => el.classList.remove('is-selected'));
|
|
165
246
|
item.classList.add('is-selected');
|
|
166
247
|
const valueEl = dd.querySelector('[data-dropdown-trigger] .ui-dropdown__value');
|
|
167
248
|
const label = item.querySelector('.ui-dropdown__label');
|
|
@@ -177,6 +258,23 @@ export function wireDropdown(root = document) {
|
|
|
177
258
|
const panel = dd.querySelector('[data-dropdown-panel]');
|
|
178
259
|
if (!trigger) return;
|
|
179
260
|
|
|
261
|
+
// Portal: lift the panel onto <body>. An ancestor whose overflow is not
|
|
262
|
+
// `visible` clips it on both axes, and one that is `position: sticky` opens
|
|
263
|
+
// a stacking context whatever z-index the panel carries — the app rail is
|
|
264
|
+
// both at once. why: docs/specification.md#the-dropdown-panel
|
|
265
|
+
if (panel && dd.hasAttribute('data-dropdown-portal')) {
|
|
266
|
+
sweepOrphanPanels();
|
|
267
|
+
panel.setAttribute('data-dropdown-portal', '');
|
|
268
|
+
panel.__ddOwner = dd;
|
|
269
|
+
dd.__ddPanel = panel;
|
|
270
|
+
document.body.appendChild(panel);
|
|
271
|
+
if (dd.classList.contains('open')) {
|
|
272
|
+
ddResolveDirection(dd, panel);
|
|
273
|
+
positionPortalPanel(dd, panel);
|
|
274
|
+
panel.classList.add('is-open');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
180
278
|
trigger.addEventListener('click', (e) => {
|
|
181
279
|
e.stopPropagation();
|
|
182
280
|
if (dd.classList.contains('open')) closeDropdown(dd);
|
|
@@ -196,7 +294,7 @@ export function wireDropdown(root = document) {
|
|
|
196
294
|
});
|
|
197
295
|
}
|
|
198
296
|
|
|
199
|
-
|
|
297
|
+
const onKeydown = (e) => {
|
|
200
298
|
const open = dd.classList.contains('open');
|
|
201
299
|
const onTrigger = e.target === trigger;
|
|
202
300
|
if ((e.key === 'ArrowDown' || e.key === 'ArrowUp') && (onTrigger || open)) {
|
|
@@ -217,7 +315,22 @@ export function wireDropdown(root = document) {
|
|
|
217
315
|
} else if (e.key === 'Tab' && open) {
|
|
218
316
|
closeDropdown(dd);
|
|
219
317
|
}
|
|
220
|
-
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
dd.addEventListener('keydown', onKeydown);
|
|
321
|
+
if (dd.__ddPanel) {
|
|
322
|
+
// A portalled panel is no longer inside the container, so a keystroke on
|
|
323
|
+
// an item never bubbles to it. Bound here only, or it would fire twice.
|
|
324
|
+
dd.__ddPanel.addEventListener('keydown', onKeydown);
|
|
325
|
+
// It is also placed once, on open, and a trigger whose box changes after
|
|
326
|
+
// that — a webfont arriving, a longer label — leaves it adrift. Scroll
|
|
327
|
+
// and resize do not see a reflow; this does.
|
|
328
|
+
if (typeof ResizeObserver === 'function') {
|
|
329
|
+
new ResizeObserver(() => {
|
|
330
|
+
if (dd.classList.contains('open')) positionPortalPanel(dd, dd.__ddPanel);
|
|
331
|
+
}).observe(trigger);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
221
334
|
});
|
|
222
335
|
|
|
223
336
|
if (!_ddGlobalWired) {
|
|
@@ -228,5 +341,14 @@ export function wireDropdown(root = document) {
|
|
|
228
341
|
const open = document.querySelector('[data-dropdown].open');
|
|
229
342
|
if (open) { closeDropdown(open); open.querySelector('[data-dropdown-trigger]')?.focus(); }
|
|
230
343
|
});
|
|
344
|
+
// Viewport coordinates go stale the moment anything scrolls. Capture, so a
|
|
345
|
+
// scroll inside the rail the panel was lifted out of counts too.
|
|
346
|
+
const reposition = () => {
|
|
347
|
+
document.querySelectorAll('[data-dropdown].open').forEach((dd) => {
|
|
348
|
+
if (dd.__ddPanel) positionPortalPanel(dd, dd.__ddPanel);
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
window.addEventListener('scroll', reposition, true);
|
|
352
|
+
window.addEventListener('resize', reposition);
|
|
231
353
|
}
|
|
232
354
|
}
|
package/src/index.css
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
* addressable — package.json `exports` has no wildcard for ./styles/*, so a deep
|
|
5
5
|
* path into src/styles/ does not resolve. To take one component, read it by name
|
|
6
6
|
* through `apliteni-ui/inline` (`import { topbarCss } from …`).
|
|
7
|
-
* Requires
|
|
7
|
+
* Requires two families, loaded by the host page or the Storybook preview:
|
|
8
|
+
* Poppins (--font-display, headings and brand marks) and IBM Plex Sans
|
|
9
|
+
* (--font-sans, everything else). Weights 300-700 in both. Neither is
|
|
10
|
+
* bundled; a family that never loads falls back to the system stack behind
|
|
11
|
+
* it, in silence.
|
|
12
|
+
* why: docs/specification.md#typefaces
|
|
8
13
|
* ========================================================================== */
|
|
9
14
|
@import "./tokens/brand.generated.css";
|
|
10
15
|
@import "./tokens/tokens.css";
|
package/src/styles/base.css
CHANGED
|
@@ -35,6 +35,28 @@ a {
|
|
|
35
35
|
text-decoration: none;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/* -- The two typeface roles -----------------------------------------------
|
|
39
|
+
* A heading is the display face, running text is the text face, and the
|
|
40
|
+
* ELEMENT decides which — never the size. A size threshold would change a
|
|
41
|
+
* heading's typeface halfway through a resize, which is the one thing a
|
|
42
|
+
* reader notices. A component that wants a heading tag set in the text face
|
|
43
|
+
* says so on its own rule, which outranks this one at (0,1,0); .ui-drawer__title
|
|
44
|
+
* and .ui-confirm__title are the two that do.
|
|
45
|
+
* why: docs/specification.md#typefaces */
|
|
46
|
+
h1, h2, h3, h4, h5, h6 {
|
|
47
|
+
font-family: var(--font-display);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* The browser default for b/strong is 700, and at the 13px the kit's tables and
|
|
51
|
+
* chat feeds run at, 700 stops reading as emphasis and starts reading as a
|
|
52
|
+
* filled-in shape. The semibold step is the emphasis; the bold step stays
|
|
53
|
+
* available to anything that asks for it by name.
|
|
54
|
+
* why: docs/specification.md#typefaces */
|
|
55
|
+
b,
|
|
56
|
+
strong {
|
|
57
|
+
font-weight: var(--weight-semibold);
|
|
58
|
+
}
|
|
59
|
+
|
|
38
60
|
/* Ambient blurred glow — the deck's signature background depth.
|
|
39
61
|
Place <span class="ui-glow ui-glow--purple"> absolutely inside a positioned
|
|
40
62
|
parent. Two colour variants ship; scale/position with inline styles. */
|
package/src/styles/confirm.css
CHANGED
|
@@ -85,7 +85,10 @@
|
|
|
85
85
|
/* -- Slots ----------------------------------------------------------------- */
|
|
86
86
|
/* The question and the answers keep their size; the consequence between them is
|
|
87
87
|
the one that gives, and scrolls — the same split the drawer's header / body /
|
|
88
|
-
footer makes.
|
|
88
|
+
footer makes. The title is an h2 and opts out of the display face base.css
|
|
89
|
+
gives a heading, for the same reason .ui-drawer__title does: at --text-md it
|
|
90
|
+
is the question on a dialog, set at the size the answer beneath it is set at.
|
|
91
|
+
why: docs/specification.md#typefaces */
|
|
89
92
|
.ui-confirm__title {
|
|
90
93
|
margin: 0;
|
|
91
94
|
flex: none;
|
package/src/styles/drawer.css
CHANGED
|
@@ -119,6 +119,10 @@
|
|
|
119
119
|
border-bottom: 1px solid var(--border);
|
|
120
120
|
flex: none;
|
|
121
121
|
}
|
|
122
|
+
/* An h2, and base.css would give it the display face. It opts out: at 15px in a
|
|
123
|
+
panel header it is a label on a piece of UI rather than a heading anybody
|
|
124
|
+
reads, and the display face at that size is exactly what #253 was about.
|
|
125
|
+
why: docs/specification.md#typefaces */
|
|
122
126
|
.ui-drawer__title {
|
|
123
127
|
margin: 0;
|
|
124
128
|
font-family: var(--font-sans);
|
package/src/styles/dropdown.css
CHANGED
|
@@ -41,8 +41,20 @@
|
|
|
41
41
|
|
|
42
42
|
/* Panel — the popover surface */
|
|
43
43
|
.ui-dropdown__panel {
|
|
44
|
+
/* One number for the trigger-to-panel offset, read by both edges here and by
|
|
45
|
+
the portal's JS, so an upward panel cannot drift from a downward one.
|
|
46
|
+
why: docs/specification.md#the-dropdown-panel */
|
|
47
|
+
--ui-dropdown-gap: 9px;
|
|
48
|
+
/* The panel states its own role rather than inheriting one, because `portal:
|
|
49
|
+
true` moves it onto <body> and an inherited face is then decided by where
|
|
50
|
+
the panel was MOUNTED rather than by what it is. Measured: the same dropdown
|
|
51
|
+
inside a display-face subtree resolved to var(--font-display) in place and
|
|
52
|
+
var(--font-sans) once portalled — a positioning flag changing the typeface.
|
|
53
|
+
A list of item rows is text, so it is the text face in both placements.
|
|
54
|
+
why: docs/specification.md#typefaces */
|
|
55
|
+
font-family: var(--font-sans);
|
|
44
56
|
position: absolute;
|
|
45
|
-
top: calc(100% +
|
|
57
|
+
top: calc(100% + var(--ui-dropdown-gap));
|
|
46
58
|
left: 0;
|
|
47
59
|
min-width: 240px;
|
|
48
60
|
background: var(--surface-2);
|
|
@@ -63,8 +75,33 @@
|
|
|
63
75
|
}
|
|
64
76
|
.ui-dropdown__panel.is-end { left: auto; right: 0; }
|
|
65
77
|
.ui-dropdown__panel.is-scroll { max-height: 300px; overflow-y: auto; }
|
|
78
|
+
|
|
79
|
+
/* Opening upward. The kit releases its own `top` here so that nothing outside
|
|
80
|
+
the kit has to: an absolute box with both edges pinned is stretched between
|
|
81
|
+
them, and a consumer who set `bottom` and left our `top` standing got a
|
|
82
|
+
fourteen-pixel panel. The entry travel is mirrored too, so the panel arrives
|
|
83
|
+
from the trigger whichever way it opens.
|
|
84
|
+
why: docs/specification.md#the-dropdown-panel */
|
|
85
|
+
.ui-dropdown__panel.is-up {
|
|
86
|
+
top: auto;
|
|
87
|
+
bottom: calc(100% + var(--ui-dropdown-gap));
|
|
88
|
+
transform: translateY(6px);
|
|
89
|
+
}
|
|
90
|
+
|
|
66
91
|
.ui-dropdown.open .ui-dropdown__panel { opacity: 1; visibility: visible; transform: translateY(0); }
|
|
67
92
|
|
|
93
|
+
/* Portalled panel — wireDropdown() moves it onto <body>, clear of every
|
|
94
|
+
ancestor's overflow and every ancestor's stacking context. The descendant
|
|
95
|
+
selector above stops matching the moment it leaves the trigger's subtree, so
|
|
96
|
+
its open state is a class of its own; the wiring writes the viewport
|
|
97
|
+
coordinates inline, and these `auto`s are what it writes over.
|
|
98
|
+
why: docs/specification.md#the-dropdown-panel */
|
|
99
|
+
.ui-dropdown__panel--portal {
|
|
100
|
+
position: fixed;
|
|
101
|
+
top: auto; right: auto; bottom: auto; left: auto;
|
|
102
|
+
}
|
|
103
|
+
.ui-dropdown__panel--portal.is-open { opacity: 1; visibility: visible; transform: translateY(0); }
|
|
104
|
+
|
|
68
105
|
/* Item row — [icon] [main: label + desc] [badge] [tick] */
|
|
69
106
|
.ui-dropdown__item {
|
|
70
107
|
display: flex;
|
package/src/styles/layout.css
CHANGED
|
@@ -55,6 +55,9 @@
|
|
|
55
55
|
gap: var(--space-2);
|
|
56
56
|
padding: 2px 6px;
|
|
57
57
|
text-decoration: none;
|
|
58
|
+
/* display: brand — the rail's lockup is the same mark topbar.css sets, and the
|
|
59
|
+
two have to be the same face or the shell reads as two products. */
|
|
60
|
+
font-family: var(--font-display);
|
|
58
61
|
font-weight: var(--weight-semibold);
|
|
59
62
|
letter-spacing: var(--tracking-tight);
|
|
60
63
|
}
|
package/src/styles/topbar.css
CHANGED
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
display: inline-flex;
|
|
31
31
|
align-items: center;
|
|
32
32
|
gap: 9px;
|
|
33
|
+
/* display: brand — a wordmark is the mark, not text. It is two words beside a
|
|
34
|
+
glyph and it is what the product is recognised by, so it keeps the display
|
|
35
|
+
face at every size it is set at, including the 13px below. */
|
|
36
|
+
font-family: var(--font-display);
|
|
33
37
|
font-weight: 600;
|
|
34
38
|
color: var(--strong);
|
|
35
39
|
text-decoration: none;
|
package/src/tokens/tokens.css
CHANGED
|
@@ -42,8 +42,17 @@
|
|
|
42
42
|
--container: 1120px;
|
|
43
43
|
--measure: 860px;
|
|
44
44
|
|
|
45
|
-
/* -- Typography ---------------------------------------------------------
|
|
46
|
-
|
|
45
|
+
/* -- Typography ---------------------------------------------------------
|
|
46
|
+
* Two roles, not one family. --font-display is the brand voice — headings,
|
|
47
|
+
* brand lockups, large readouts — and --font-sans is what an application is
|
|
48
|
+
* actually made of: tables, fields, paragraphs, chat. Poppins is a geometric
|
|
49
|
+
* display grotesque and holds its character at heading sizes; at 13-14px it
|
|
50
|
+
* smears, worst of all in Cyrillic, which is where the kit spends most of its
|
|
51
|
+
* pixels. IBM Plex Sans is humanist and narrower, and its Cyrillic was drawn
|
|
52
|
+
* by the same team as its Latin.
|
|
53
|
+
* why: docs/specification.md#typefaces */
|
|
54
|
+
--font-display: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
55
|
+
--font-sans: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
47
56
|
--font-mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
|
48
57
|
|
|
49
58
|
--text-xs: 11px;
|