@apliteni/apliteni-ui 0.33.0 → 0.34.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/package.json +1 -1
- package/react/README.md +87 -1
- package/react/dist/index.css +2 -1
- package/react/dist/index.d.ts +122 -2
- package/react/dist/index.js +442 -11
- package/src/components/dropdown.js +36 -15
- package/src/components/shell.js +124 -37
- package/src/components/tooltip.js +3 -0
- package/src/styles/back.css +12 -1
- package/src/styles/callout.css +8 -1
- package/src/styles/command-palette.css +9 -2
- package/src/styles/confirm.css +9 -2
- package/src/styles/drawer.css +25 -5
- package/src/styles/dropdown.css +41 -11
- package/src/styles/layout.css +116 -9
- package/src/styles/tooltip.css +4 -1
- package/src/styles/topbar.css +10 -3
- package/src/tokens/tokens.css +27 -6
package/src/components/shell.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// The kit's one page shell: a full-height rail beside one <main
|
|
2
|
-
// is a thin preset over it that keeps the topbar. wireShell() once
|
|
3
|
-
// wires the fold, the nav's groups and the reader's menu.
|
|
1
|
+
// The kit's one page shell: a full-height rail beside one <main>, in two layouts.
|
|
2
|
+
// accountShell() is a thin preset over it that keeps the topbar. wireShell() once
|
|
3
|
+
// after mounting wires the fold, the nav's groups and the reader's menu.
|
|
4
4
|
// why: docs/specification.md#the-page-shell
|
|
5
5
|
import { topbar as productTopbar } from './topbar.js';
|
|
6
6
|
import { esc, icon } from './index.js';
|
|
@@ -9,6 +9,7 @@ import { dropdown, wireDropdown } from './dropdown.js';
|
|
|
9
9
|
import { backLink } from './back.js';
|
|
10
10
|
import { prism } from '../assets/brand.js';
|
|
11
11
|
import { ACCOUNT_NAV, toMenuTuple, initials } from './account-nav.js';
|
|
12
|
+
import { paletteHotkey } from './command-palette.js';
|
|
12
13
|
|
|
13
14
|
// The one account navigation definition lives in account-nav.js because topbar.js needs
|
|
14
15
|
// it too; re-exported here so the name docs/library.md publishes keeps working.
|
|
@@ -78,6 +79,26 @@ const toTopbar = (t) => {
|
|
|
78
79
|
return out;
|
|
79
80
|
};
|
|
80
81
|
|
|
82
|
+
// ---- the two layouts, and the two widths ---------------------------------
|
|
83
|
+
//
|
|
84
|
+
// Each is a name the kit knows or the default — read strictly, and not through
|
|
85
|
+
// String(), which turned `['topbar']` into a layout and drew half a page.
|
|
86
|
+
// why: docs/specification.md#the-page-shell
|
|
87
|
+
const toLayout = (v) => (v === 'topbar' ? 'topbar' : 'rail');
|
|
88
|
+
|
|
89
|
+
// why: docs/specification.md#widths
|
|
90
|
+
const toWidth = (v) => (v === 'wide' ? 'wide' : 'centered');
|
|
91
|
+
|
|
92
|
+
// `search: 'palette-id'`, or the same id in `{ palette }` with a placeholder beside
|
|
93
|
+
// it. No palette to open, no field — the argument `signOutHref` takes.
|
|
94
|
+
// why: docs/specification.md#the-page-shell
|
|
95
|
+
const toSearch = (v) => {
|
|
96
|
+
const given = typeof v === 'string' ? { palette: v } : (isRecord(v) && !Array.isArray(v) ? v : null);
|
|
97
|
+
const palette = given && typeof given.palette === 'string' ? given.palette.trim() : '';
|
|
98
|
+
if (!palette) return null;
|
|
99
|
+
return { palette, placeholder: str(given.placeholder) || 'Search or run a command…' };
|
|
100
|
+
};
|
|
101
|
+
|
|
81
102
|
// `maxWidth` lands inside a style attribute, so a length is all this accepts — a number
|
|
82
103
|
// and a unit, or `none`. Anything else yields '' and the caller writes no style
|
|
83
104
|
// attribute, falling through to --measure. It must REMOVE the property rather than pass
|
|
@@ -137,6 +158,7 @@ const railToggle = (collapsed) =>
|
|
|
137
158
|
// this file re-checks a value that has been through here.
|
|
138
159
|
const SHAPES = {
|
|
139
160
|
nav: toItems, crumbs: toCrumbs, back: toBack, account: toReader, maxWidth: mainMax, topbar: toTopbar,
|
|
161
|
+
layout: toLayout, width: toWidth, search: toSearch,
|
|
140
162
|
// Drawn by default; `collapsible: false` is the way out, for a page that will
|
|
141
163
|
// never call wireShell(). why: docs/specification.md#the-page-shell
|
|
142
164
|
collapsible: (v) => v !== false,
|
|
@@ -160,44 +182,71 @@ function settle(options) {
|
|
|
160
182
|
// the fold takes away. `named` is the accessible name when nothing else carries
|
|
161
183
|
// one; under the menu trigger it is null, because the button is named by the words
|
|
162
184
|
// inside it. why: docs/specification.md#the-page-shell
|
|
163
|
-
const readerFace = (name, email, named) =>
|
|
185
|
+
const readerFace = (name, email, named, markOnly = false) =>
|
|
164
186
|
`<span class="ui-app__av"${named ? ` role="img" aria-label="Signed in as ${esc(named)}"` : ' aria-hidden="true"'}>`
|
|
165
187
|
+ `${esc(initials(name, email))}</span>`
|
|
166
|
-
+ `<span class="ui-app__who"${named ? ' aria-hidden="true"' : ''}>`
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
188
|
+
+ (markOnly ? '' : `<span class="ui-app__who"${named ? ' aria-hidden="true"' : ''}>`
|
|
189
|
+
+ (name ? `<b>${esc(name)}</b>` : '')
|
|
190
|
+
+ (email ? `<span>${esc(email)}</span>` : '')
|
|
191
|
+
+ `</span>`);
|
|
170
192
|
|
|
171
193
|
// Who is signed in, and the one action on the session. A sibling of the <nav>, not
|
|
172
194
|
// its footer: a name and address are not navigation. Given a sign-out href the block
|
|
173
195
|
// is the trigger of the kit's own dropdown(), with Sign out inside it; without one
|
|
174
196
|
// there is no menu, and with nobody signed in there is no block.
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
|
|
197
|
+
//
|
|
198
|
+
// One block, two places: only the direction it opens is the layout's answer.
|
|
199
|
+
// `portal: true` either way — the rail and the band are both sticky.
|
|
200
|
+
function readerBlock({ name, email }, signOutHref, { band = false } = {}) {
|
|
178
201
|
if (!name && !email) return '';
|
|
202
|
+
const cls = `ui-app__user${band ? ' ui-app__user--bar' : ''}`;
|
|
179
203
|
const who = [name, email].filter(Boolean).join(', ');
|
|
204
|
+
// The name is said once, by whichever part is on screen: the two lines in the rail,
|
|
205
|
+
// an aria-label on the band where the mark is the whole trigger.
|
|
206
|
+
const face = band ? readerFace(name, email, null, true) : readerFace(name, email, null);
|
|
180
207
|
if (!signOutHref) {
|
|
181
|
-
return `<div class="
|
|
208
|
+
return `<div class="${cls}">${readerFace(name, email, who, band)}</div>`;
|
|
182
209
|
}
|
|
183
|
-
// The head says who the menu belongs to — and on a folded rail
|
|
184
|
-
// a sighted reader can read the address.
|
|
210
|
+
// The head says who the menu belongs to — and on a folded rail, or on a band, it is
|
|
211
|
+
// the only place a sighted reader can read the address.
|
|
185
212
|
const head = `<div class="ui-dropdown__head">`
|
|
186
213
|
+ (name ? `<b>${esc(name)}</b>` : '')
|
|
187
214
|
+ (email ? `<span>${esc(email)}</span>` : '')
|
|
188
215
|
+ `</div>`;
|
|
189
|
-
return `<div class="
|
|
216
|
+
return `<div class="${cls}">${dropdown({
|
|
190
217
|
variant: 'menu',
|
|
191
218
|
portal: true,
|
|
192
|
-
direction: 'up',
|
|
219
|
+
direction: band ? 'down' : 'up',
|
|
220
|
+
align: band ? 'end' : 'start',
|
|
221
|
+
chevron: !band,
|
|
222
|
+
ariaLabel: band ? `Signed in as ${who}` : undefined,
|
|
193
223
|
triggerClass: 'ui-app__user-trigger',
|
|
194
|
-
triggerContent:
|
|
224
|
+
triggerContent: face,
|
|
195
225
|
panelClass: 'ui-app__user-panel',
|
|
196
226
|
header: head,
|
|
197
227
|
items: [{ label: 'Sign out', icon: 'logout', href: signOutHref, danger: true }],
|
|
198
228
|
})}</div>`;
|
|
199
229
|
}
|
|
200
230
|
|
|
231
|
+
// A field to look at, a button to press. `data-cmdk-open` and the cap's class are the
|
|
232
|
+
// palette's own, and the cap is NOT aria-hidden: it is half the button's name.
|
|
233
|
+
// why: docs/specification.md#the-page-shell
|
|
234
|
+
const searchField = ({ palette, placeholder }) =>
|
|
235
|
+
`<button type="button" class="ui-app__search" data-cmdk-open="${esc(palette)}" aria-haspopup="dialog">`
|
|
236
|
+
+ `<span class="ui-app__search-ic" aria-hidden="true">${icon('search')}</span>`
|
|
237
|
+
+ `<span class="ui-app__search-txt">${esc(placeholder)}</span>`
|
|
238
|
+
+ `<kbd class="ui-cmdk__key" data-palette-hotkey>${esc(paletteHotkey())}</kbd>`
|
|
239
|
+
+ `</button>`;
|
|
240
|
+
|
|
241
|
+
// The band: search at its start, the reader at its end, beside the rail rather than
|
|
242
|
+
// across the top of both. why: docs/specification.md#the-page-shell
|
|
243
|
+
const shellBar = (search, account, signOutHref) =>
|
|
244
|
+
`<header class="ui-app__bar">`
|
|
245
|
+
+ (search ? searchField(search) : '')
|
|
246
|
+
+ `<span class="ui-app__bar-gap"></span>`
|
|
247
|
+
+ readerBlock(account, signOutHref, { band: true })
|
|
248
|
+
+ `</header>`;
|
|
249
|
+
|
|
201
250
|
// Unique-per-render suffix for the brand mark's clip id — the same reason
|
|
202
251
|
// nav.js keeps a module counter. Two shells on one page must not collide.
|
|
203
252
|
let _shellUid = 0;
|
|
@@ -222,7 +271,12 @@ export function appShell(options = {}) {
|
|
|
222
271
|
maxWidth,
|
|
223
272
|
collapsible,
|
|
224
273
|
collapsed,
|
|
274
|
+
layout,
|
|
275
|
+
width,
|
|
276
|
+
search,
|
|
225
277
|
} = settle(options);
|
|
278
|
+
// The layout's one branch, read once. Everything below asks this and not the string.
|
|
279
|
+
const banded = layout === 'topbar';
|
|
226
280
|
const up = back ? backLink(back) : '';
|
|
227
281
|
// No footer slot: the nav is places to go, and the one thing that was in it —
|
|
228
282
|
// sign out — is in the reader's menu at the rail's foot.
|
|
@@ -232,10 +286,15 @@ export function appShell(options = {}) {
|
|
|
232
286
|
activeIs: up ? 'section' : 'page',
|
|
233
287
|
ariaLabel: navLabel,
|
|
234
288
|
});
|
|
289
|
+
// One band over a page, never two: the banded layout draws its own, so the
|
|
290
|
+
// compatibility bag is not drawn beside it. why: docs/specification.md#the-page-shell
|
|
291
|
+
const compat = banded ? null : topbar;
|
|
235
292
|
// The topbar already says the product word, so the rail head steps aside when there is
|
|
236
293
|
// one. The word is the link's only text and the narrow rail folds it out of view, so
|
|
237
|
-
// the name is written out — the mark itself is aria-hidden.
|
|
238
|
-
|
|
294
|
+
// the name is written out — the mark itself is aria-hidden. The banded layout's own
|
|
295
|
+
// bar says no word, so there the rail keeps the lockup: that layout's head band IS
|
|
296
|
+
// the product's mark. why: docs/specification.md#the-page-shell
|
|
297
|
+
const brand = compat ? '' : `<a class="ui-app__brand" href="${esc(brandHref)}" aria-label="${esc(word)}">`
|
|
239
298
|
+ `${prism(`appb-${++_shellUid}`, 24)}<span>${esc(word)}</span></a>`;
|
|
240
299
|
// A <div>, not an <aside>: <aside> is the `complementary` landmark, and this holds the
|
|
241
300
|
// page's primary navigation and the signed-in reader. The <nav> inside it is already
|
|
@@ -244,27 +303,40 @@ export function appShell(options = {}) {
|
|
|
244
303
|
// caller left the choice to the reader; wireShell() applies the stored one there.
|
|
245
304
|
const folded = collapsible && collapsed === true;
|
|
246
305
|
const auto = collapsible && collapsed === null ? ' data-rail="auto"' : '';
|
|
306
|
+
// Where this stands is the one thing the two layouts disagree about on the rail.
|
|
307
|
+
const fold = collapsible ? railToggle(folded) : '';
|
|
247
308
|
// The head band: the product's mark, and the rail's own control at the far end of
|
|
248
309
|
// the same line, under one rule. Either may be absent — a shell with a topbar says
|
|
249
310
|
// the word up there, and `collapsible: false` draws no toggle — so the band itself
|
|
250
311
|
// goes when both are.
|
|
251
|
-
const
|
|
252
|
-
|
|
312
|
+
const inHead = banded ? '' : fold;
|
|
313
|
+
const head = brand || inHead
|
|
314
|
+
? `<div class="ui-app__head">${brand}${inHead}</div>`
|
|
253
315
|
: '';
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
<main class="ui-app__main"${maxWidth ? ` style="--ui-app-main: ${maxWidth}"` : ''}>
|
|
316
|
+
// The rail's foot: the reader, or the control that took their place. One block
|
|
317
|
+
// either way. why: docs/specification.md#the-page-shell
|
|
318
|
+
const foot = banded
|
|
319
|
+
? (fold ? `<div class="ui-app__foot">${fold}</div>` : '')
|
|
320
|
+
: readerBlock(account, signOutHref);
|
|
321
|
+
const main = `<main class="ui-app__main${width === 'wide' ? ' ui-app__main--wide' : ''}"${maxWidth ? ` style="--ui-app-main: ${maxWidth}"` : ''}>
|
|
261
322
|
${up || (crumbs.length ? breadcrumbs({ items: crumbs }) : '')}
|
|
262
323
|
${title ? `<h1>${title}</h1>` : ''}
|
|
263
324
|
${sub ? `<p class="ui-app__sub">${sub}</p>` : ''}
|
|
264
325
|
<div class="ui-app__body">${body}</div>
|
|
265
|
-
</main
|
|
326
|
+
</main>`;
|
|
327
|
+
// The second column is a box of its own: the band and the page are two rows of it.
|
|
328
|
+
const well = banded
|
|
329
|
+
? `<div class="ui-app__well">${shellBar(search, account, signOutHref)}${main}</div>`
|
|
330
|
+
: main;
|
|
331
|
+
const grid = `<div class="ui-app${banded ? ' ui-app--topbar' : ''}${folded ? ' is-collapsed' : ''}"${auto}>
|
|
332
|
+
<div class="ui-app__rail">
|
|
333
|
+
${head}
|
|
334
|
+
${rail}
|
|
335
|
+
${foot}
|
|
336
|
+
</div>
|
|
337
|
+
${well}
|
|
266
338
|
</div>`;
|
|
267
|
-
return
|
|
339
|
+
return compat ? `<div class="ui-app-page">${productTopbar(compat)}${grid}</div>` : grid;
|
|
268
340
|
}
|
|
269
341
|
|
|
270
342
|
// ---- Behaviour -----------------------------------------------------------
|
|
@@ -275,13 +347,14 @@ export function appShell(options = {}) {
|
|
|
275
347
|
const _wiredDocs = new WeakSet();
|
|
276
348
|
const _unpersisted = new WeakSet();
|
|
277
349
|
|
|
278
|
-
// The toggle, addressed from the rail that owns it:
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
350
|
+
// The toggle, addressed from the rail that owns it: a fold row of a shell's own rail
|
|
351
|
+
// and nowhere else, so a stray [data-rail-toggle] in the page body folds nothing. The
|
|
352
|
+
// path is written once — the listener, the reflector and wireShell() all have to mean
|
|
353
|
+
// the same control. A descendant and not a child, because the two layouts stand the
|
|
354
|
+
// row in two places: the head band, or the rail's foot.
|
|
282
355
|
// why: docs/specification.md#the-page-shell
|
|
283
|
-
const FOLD_PATH = '.ui-
|
|
284
|
-
const RAIL_FOLD = `.ui-app__rail
|
|
356
|
+
const FOLD_PATH = '.ui-app__fold-row > [data-rail-toggle]';
|
|
357
|
+
const RAIL_FOLD = `.ui-app__rail ${FOLD_PATH}`;
|
|
285
358
|
|
|
286
359
|
// Under an opted-out root? Steps out of a shadow root through its host.
|
|
287
360
|
const optedOut = (node) => {
|
|
@@ -293,7 +366,7 @@ function setRail(app, collapsed) {
|
|
|
293
366
|
app.classList.toggle('is-collapsed', collapsed);
|
|
294
367
|
const rail = app.querySelector(':scope > .ui-app__rail');
|
|
295
368
|
if (!rail) return;
|
|
296
|
-
for (const btn of rail.querySelectorAll(`:scope
|
|
369
|
+
for (const btn of rail.querySelectorAll(`:scope ${FOLD_PATH}`)) {
|
|
297
370
|
btn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
|
298
371
|
btn.setAttribute('aria-label', railName(collapsed));
|
|
299
372
|
const label = btn.querySelector('.ui-nav__label');
|
|
@@ -334,6 +407,12 @@ export function wireShell(root = document, { persist } = {}) {
|
|
|
334
407
|
// The reader's menu is a dropdown() like any other, so it is wired like any
|
|
335
408
|
// other. Idempotent, and a shell with no account draws none to find.
|
|
336
409
|
wireDropdown(root);
|
|
410
|
+
// A server cannot know which key the reader holds, so the browser corrects it here —
|
|
411
|
+
// off the ROOT's window, not the global one. why: docs/specification.md#the-page-shell
|
|
412
|
+
const here = root.nodeType === 9 ? root : root.ownerDocument;
|
|
413
|
+
const view = here && here.defaultView;
|
|
414
|
+
const key = paletteHotkey(view && view.navigator ? view.navigator.platform : '');
|
|
415
|
+
for (const kbd of root.querySelectorAll('[data-palette-hotkey]')) kbd.textContent = key;
|
|
337
416
|
const doc = root.nodeType === 9 ? root : root.ownerDocument;
|
|
338
417
|
listen(doc);
|
|
339
418
|
if (persist === false) _unpersisted.add(root);
|
|
@@ -368,6 +447,9 @@ export function accountShell({
|
|
|
368
447
|
signOutHref = '#logout',
|
|
369
448
|
collapsible = true,
|
|
370
449
|
collapsed,
|
|
450
|
+
layout,
|
|
451
|
+
width,
|
|
452
|
+
search,
|
|
371
453
|
} = {}) {
|
|
372
454
|
// The same normaliser appShell() runs, called once here so the rail and the
|
|
373
455
|
// topbar menu are handed one list rather than two readings of `nav`.
|
|
@@ -389,6 +471,11 @@ export function accountShell({
|
|
|
389
471
|
signOutHref,
|
|
390
472
|
collapsible,
|
|
391
473
|
collapsed,
|
|
474
|
+
// Settled nowhere but appShell(). Under `layout: 'topbar'` the bag below is not
|
|
475
|
+
// drawn, which is what that layout costs the preset.
|
|
476
|
+
layout,
|
|
477
|
+
width,
|
|
478
|
+
search,
|
|
392
479
|
topbar: {
|
|
393
480
|
word,
|
|
394
481
|
view: 'text',
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
import { esc } from './index.js';
|
|
15
15
|
|
|
16
16
|
const cx = (...a) => a.filter(Boolean).join(' ');
|
|
17
|
+
/* classes: ui-tip__label ui-tip__value ui-tip__detail — built from `k` below, so
|
|
18
|
+
a sweep reading class names off the source cannot see them.
|
|
19
|
+
why: CONTRIBUTING.md#a-spelling-the-sweep-cannot-see-costs-coverage-in-silence */
|
|
17
20
|
const PARTS = ['label', 'value', 'detail'];
|
|
18
21
|
const px = (n) => (typeof n === 'number' && Number.isFinite(n) ? `${n}px` : null);
|
|
19
22
|
let seq = 0;
|
package/src/styles/back.css
CHANGED
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
* ========================================================================== */
|
|
8
8
|
|
|
9
9
|
/* Its own line, and only as wide as its words, so the hover ground does not run
|
|
10
|
-
the width of the column
|
|
10
|
+
the width of the column — and never wider than it, which `fit-content` alone
|
|
11
|
+
does not give once the label below is `nowrap`. why: #303 */
|
|
11
12
|
.ui-back {
|
|
12
13
|
display: flex;
|
|
13
14
|
width: fit-content;
|
|
15
|
+
max-width: 100%;
|
|
14
16
|
align-items: center;
|
|
15
17
|
gap: var(--space-1);
|
|
16
18
|
/* WCAG 2.5.8's 24px: the line of text alone is 17px tall. */
|
|
@@ -40,3 +42,12 @@
|
|
|
40
42
|
/* In the page shell it takes the trail's place, and the trail's distance from
|
|
41
43
|
the title with it. */
|
|
42
44
|
.ui-app__main > .ui-back { margin-bottom: var(--space-5); }
|
|
45
|
+
|
|
46
|
+
/* The one part that can run long. It clips rather than wrapping, as .ui-nav__label
|
|
47
|
+
does. why: docs/specification.md#the-back-link */
|
|
48
|
+
.ui-back__label {
|
|
49
|
+
min-width: 0;
|
|
50
|
+
overflow: hidden;
|
|
51
|
+
text-overflow: ellipsis;
|
|
52
|
+
white-space: nowrap;
|
|
53
|
+
}
|
package/src/styles/callout.css
CHANGED
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
width: 100%;
|
|
45
45
|
max-width: var(--panel-md);
|
|
46
46
|
overflow: hidden; /* clips the timer bar + left marker to the rounded corners */
|
|
47
|
+
/* A toast floats. It carries a status rather than the neutral hairline, so each
|
|
48
|
+
style below re-points --elev-edge at its own tint and leaves the outer line
|
|
49
|
+
where it already drew it. why: docs/specification.md#elevation */
|
|
50
|
+
box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop);
|
|
47
51
|
animation: ui-toast-in var(--dur-med) var(--ease-out) both;
|
|
48
52
|
}
|
|
49
53
|
|
|
@@ -115,6 +119,7 @@
|
|
|
115
119
|
.ui-toast--soft {
|
|
116
120
|
background: var(--toast-glow);
|
|
117
121
|
border: 1px solid color-mix(in srgb, var(--toast-accent) 22%, transparent);
|
|
122
|
+
--elev-edge: color-mix(in srgb, var(--toast-accent) 11%, transparent);
|
|
118
123
|
}
|
|
119
124
|
|
|
120
125
|
/* style: solid — the status at fill strength, inked with the pole opposite it.
|
|
@@ -128,6 +133,8 @@
|
|
|
128
133
|
--toast-ink: var(--signal-solid-ink);
|
|
129
134
|
background: var(--toast-solid);
|
|
130
135
|
color: var(--toast-ink);
|
|
136
|
+
/* No inner line: a solid toast has no outer one either. It keeps the drop. */
|
|
137
|
+
--elev-edge: transparent;
|
|
131
138
|
}
|
|
132
139
|
.ui-toast--solid .ui-toast__title { color: var(--toast-ink); }
|
|
133
140
|
/* The body copy is the quieter of the two, but it stays quiet by dilution the
|
|
@@ -147,7 +154,7 @@
|
|
|
147
154
|
.ui-toast--solid .ui-toast__timer { background: color-mix(in srgb, var(--toast-ink) 55%, transparent); }
|
|
148
155
|
|
|
149
156
|
/* style: outline — elevated card with a status-tinted border */
|
|
150
|
-
.ui-toast--outline { background: var(--bg-elevated); border: 1px solid color-mix(in srgb, var(--toast-accent) 40%, transparent); }
|
|
157
|
+
.ui-toast--outline { background: var(--bg-elevated); border: 1px solid color-mix(in srgb, var(--toast-accent) 40%, transparent); --elev-edge: color-mix(in srgb, var(--toast-accent) 20%, transparent); }
|
|
151
158
|
|
|
152
159
|
/* compact — single line, no body text */
|
|
153
160
|
.ui-toast--compact { align-items: center; padding-top: 10px; padding-bottom: 10px; }
|
|
@@ -69,7 +69,10 @@
|
|
|
69
69
|
flex-direction: column;
|
|
70
70
|
font-family: var(--font-sans);
|
|
71
71
|
background: var(--cmdk-surface);
|
|
72
|
-
|
|
72
|
+
/* The two-step edge and the drop: --border-strong outside, --border one pixel
|
|
73
|
+
inside it, then the broad faint drop. why: docs/specification.md#elevation */
|
|
74
|
+
border: 1px solid var(--border-strong);
|
|
75
|
+
box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop);
|
|
73
76
|
border-radius: var(--radius-lg);
|
|
74
77
|
overflow: hidden;
|
|
75
78
|
opacity: 0;
|
|
@@ -79,7 +82,11 @@
|
|
|
79
82
|
transform var(--dur-med) var(--ease);
|
|
80
83
|
}
|
|
81
84
|
.ui-cmdk.is-open .ui-cmdk__panel { opacity: 1; transform: translate(-50%, 0); }
|
|
82
|
-
|
|
85
|
+
/* The ring is COMPOSED with the floating treatment, not written over it: a
|
|
86
|
+
box-shadow list replaces the whole list, so `box-shadow: var(--ring)` alone
|
|
87
|
+
took the panel's edge and its drop off for as long as it held focus.
|
|
88
|
+
why: docs/specification.md#elevation */
|
|
89
|
+
.ui-cmdk__panel:focus-visible { outline: none; box-shadow: var(--ring), inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop); }
|
|
83
90
|
|
|
84
91
|
/* -- The text box --------------------------------------------------------- */
|
|
85
92
|
|
package/src/styles/confirm.css
CHANGED
|
@@ -62,7 +62,10 @@
|
|
|
62
62
|
gap: var(--space-2);
|
|
63
63
|
padding: var(--space-5);
|
|
64
64
|
background: var(--confirm-surface);
|
|
65
|
-
|
|
65
|
+
/* The two-step edge and the drop: --border-strong outside, --border one pixel
|
|
66
|
+
inside it, then the broad faint drop. why: docs/specification.md#elevation */
|
|
67
|
+
border: 1px solid var(--border-strong);
|
|
68
|
+
box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop);
|
|
66
69
|
border-radius: var(--radius-lg);
|
|
67
70
|
opacity: 0;
|
|
68
71
|
transform: translateY(8px);
|
|
@@ -71,7 +74,11 @@
|
|
|
71
74
|
transform var(--dur-med) var(--ease);
|
|
72
75
|
}
|
|
73
76
|
.ui-confirm.is-open .ui-confirm__panel { opacity: 1; transform: none; }
|
|
74
|
-
|
|
77
|
+
/* The ring is COMPOSED with the floating treatment, not written over it: a
|
|
78
|
+
box-shadow list replaces the whole list, so `box-shadow: var(--ring)` alone
|
|
79
|
+
took the panel's edge and its drop off for as long as it held focus.
|
|
80
|
+
why: docs/specification.md#elevation */
|
|
81
|
+
.ui-confirm__panel:focus-visible { outline: none; box-shadow: var(--ring), inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop); }
|
|
75
82
|
|
|
76
83
|
/* -- Slots ----------------------------------------------------------------- */
|
|
77
84
|
/* The question and the answers keep their size; the consequence between them is
|
package/src/styles/drawer.css
CHANGED
|
@@ -58,9 +58,19 @@
|
|
|
58
58
|
display: flex;
|
|
59
59
|
flex-direction: column;
|
|
60
60
|
background: var(--drawer-surface);
|
|
61
|
+
/* A drawer is flush to a screen edge, so it has one edge rather than four and the
|
|
62
|
+
two-step edge runs along that one: --border-strong on the edge rule below, the
|
|
63
|
+
inner line one pixel inside it. This is the one floating surface that writes no
|
|
64
|
+
inset RING — a full ring would draw 1px lines across the top and bottom of a
|
|
65
|
+
full-height panel, where there is no edge — so it composes its own directional
|
|
66
|
+
line with the drops instead. The line is a variable so the focus rule can
|
|
67
|
+
compose all three layers without knowing which edge it is on; its default is a
|
|
68
|
+
no-op layer rather than `none`, which is valid only alone and would invalidate
|
|
69
|
+
the list. why: docs/specification.md#elevation */
|
|
70
|
+
--drawer-line: 0 0 0 0 transparent;
|
|
71
|
+
box-shadow: var(--drawer-line), var(--elev-drop);
|
|
61
72
|
transition: transform var(--dur-med) var(--ease);
|
|
62
73
|
}
|
|
63
|
-
.ui-drawer__panel:focus-visible { outline: none; box-shadow: var(--ring); }
|
|
64
74
|
.ui-drawer.is-open .ui-drawer__panel { transform: none; }
|
|
65
75
|
|
|
66
76
|
/* -- Edge anchoring + slide-in direction ---------------------------------- */
|
|
@@ -72,12 +82,14 @@
|
|
|
72
82
|
}
|
|
73
83
|
.ui-drawer--right .ui-drawer__panel {
|
|
74
84
|
right: 0;
|
|
75
|
-
border-left: 1px solid var(--border);
|
|
85
|
+
border-left: 1px solid var(--border-strong);
|
|
86
|
+
--drawer-line: inset 1px 0 0 var(--border);
|
|
76
87
|
transform: translateX(100%);
|
|
77
88
|
}
|
|
78
89
|
.ui-drawer--left .ui-drawer__panel {
|
|
79
90
|
left: 0;
|
|
80
|
-
border-right: 1px solid var(--border);
|
|
91
|
+
border-right: 1px solid var(--border-strong);
|
|
92
|
+
--drawer-line: inset -1px 0 0 var(--border);
|
|
81
93
|
transform: translateX(-100%);
|
|
82
94
|
}
|
|
83
95
|
.ui-drawer--top .ui-drawer__panel,
|
|
@@ -88,15 +100,23 @@
|
|
|
88
100
|
}
|
|
89
101
|
.ui-drawer--top .ui-drawer__panel {
|
|
90
102
|
top: 0;
|
|
91
|
-
border-bottom: 1px solid var(--border);
|
|
103
|
+
border-bottom: 1px solid var(--border-strong);
|
|
104
|
+
--drawer-line: inset 0 -1px 0 var(--border);
|
|
92
105
|
transform: translateY(-100%);
|
|
93
106
|
}
|
|
94
107
|
.ui-drawer--bottom .ui-drawer__panel {
|
|
95
108
|
bottom: 0;
|
|
96
|
-
border-top: 1px solid var(--border);
|
|
109
|
+
border-top: 1px solid var(--border-strong);
|
|
110
|
+
--drawer-line: inset 0 1px 0 var(--border);
|
|
97
111
|
transform: translateY(100%);
|
|
98
112
|
}
|
|
99
113
|
|
|
114
|
+
/* The ring is COMPOSED with the floating treatment rather than written over it — a
|
|
115
|
+
box-shadow list replaces the whole list, so a bare `box-shadow: var(--ring)` would
|
|
116
|
+
take the drawer's edge and its drop off for as long as it held focus.
|
|
117
|
+
why: docs/specification.md#elevation */
|
|
118
|
+
.ui-drawer__panel:focus-visible { outline: none; box-shadow: var(--ring), var(--drawer-line), var(--elev-drop); }
|
|
119
|
+
|
|
100
120
|
/* -- Sizes ----------------------------------------------------------------- */
|
|
101
121
|
/* left|right → width; top|bottom → height. Same names, axis chosen by side. */
|
|
102
122
|
.ui-drawer--right.ui-drawer--sm, .ui-drawer--left.ui-drawer--sm { --drawer-w: var(--panel-sm); }
|
package/src/styles/dropdown.css
CHANGED
|
@@ -28,16 +28,23 @@
|
|
|
28
28
|
.ui-dropdown__pre { color: var(--muted); }
|
|
29
29
|
.ui-dropdown__value { color: var(--strong); font-weight: 600; letter-spacing: 0.01em; }
|
|
30
30
|
|
|
31
|
-
/* Chevron — border caret that flips when the container is .open
|
|
31
|
+
/* Chevron — border caret that flips when the container is .open. Its ink is an L
|
|
32
|
+
on a rotated square, so centring the BOX leaves the mark off centre; --caret-off
|
|
33
|
+
is that gap, derived rather than typed.
|
|
34
|
+
why: docs/specification.md#the-dropdown-panel */
|
|
32
35
|
.ui-dropdown__chevron {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
--caret: 8px;
|
|
37
|
+
--caret-ink: 1.6px;
|
|
38
|
+
--caret-off: calc((var(--caret) - var(--caret-ink)) / 2.8284); /* 2√2 — the 45° turn's projection */
|
|
39
|
+
width: var(--caret); height: var(--caret);
|
|
40
|
+
border-right: var(--caret-ink) solid var(--muted);
|
|
41
|
+
border-bottom: var(--caret-ink) solid var(--muted);
|
|
42
|
+
/* Shift BEFORE rotate, so it lands in page space rather than on the turned axis. */
|
|
43
|
+
transform: translateY(calc(var(--caret-off) * -1)) rotate(45deg);
|
|
37
44
|
transition: transform var(--dur-fast) var(--ease);
|
|
38
45
|
flex: none;
|
|
39
46
|
}
|
|
40
|
-
.ui-dropdown.open .ui-dropdown__chevron { transform:
|
|
47
|
+
.ui-dropdown.open .ui-dropdown__chevron { transform: translateY(var(--caret-off)) rotate(-135deg); }
|
|
41
48
|
|
|
42
49
|
/* Panel — the popover surface */
|
|
43
50
|
.ui-dropdown__panel {
|
|
@@ -45,6 +52,10 @@
|
|
|
45
52
|
the portal's JS, so an upward panel cannot drift from a downward one.
|
|
46
53
|
why: docs/specification.md#the-dropdown-panel */
|
|
47
54
|
--ui-dropdown-gap: 9px;
|
|
55
|
+
/* Named for the same reason the offset is: a block pinned to an edge has to
|
|
56
|
+
bleed back through it, and #306 is a page copying the number by hand.
|
|
57
|
+
why: docs/specification.md#the-dropdown-panel */
|
|
58
|
+
--ui-dropdown-pad: 6px;
|
|
48
59
|
/* The panel states its own role rather than inheriting one, because `portal:
|
|
49
60
|
true` moves it onto <body> and an inherited face is then decided by where
|
|
50
61
|
the panel was MOUNTED rather than by what it is. Measured: the same dropdown
|
|
@@ -60,9 +71,12 @@
|
|
|
60
71
|
/* The floating step, not the sunken one — a panel held open over a card cannot be
|
|
61
72
|
painted the colour of an inset. why: docs/specification.md#elevation */
|
|
62
73
|
background: var(--bg-elevated);
|
|
63
|
-
|
|
74
|
+
/* The two-step edge and the drop: --border-strong outside, --border one pixel
|
|
75
|
+
inside it, then the broad faint drop. why: docs/specification.md#elevation */
|
|
76
|
+
border: 1px solid var(--border-strong);
|
|
77
|
+
box-shadow: inset 0 0 0 1px var(--elev-edge, var(--border)), var(--elev-drop);
|
|
64
78
|
border-radius: var(--radius-lg);
|
|
65
|
-
padding:
|
|
79
|
+
padding: var(--ui-dropdown-pad);
|
|
66
80
|
opacity: 0;
|
|
67
81
|
visibility: hidden;
|
|
68
82
|
/* Drawn for the whole of the fade out, so the rows do not vanish mid-fade — and a
|
|
@@ -200,13 +214,23 @@
|
|
|
200
214
|
/* Separator */
|
|
201
215
|
.ui-dropdown__sep { height: 1px; background: var(--border); margin: 5px 4px; }
|
|
202
216
|
|
|
203
|
-
/* Optional pinned
|
|
204
|
-
|
|
217
|
+
/* Optional pinned head/foot blocks — symmetrical: one inner padding, and each
|
|
218
|
+
bleeding to the edge it sits on, lining the edge it faces.
|
|
219
|
+
why: docs/specification.md#the-dropdown-panel */
|
|
220
|
+
.ui-dropdown__panel > .ui-dropdown__head,
|
|
221
|
+
.ui-dropdown__panel > .ui-dropdown__foot {
|
|
205
222
|
padding: 11px 13px;
|
|
206
|
-
|
|
223
|
+
}
|
|
224
|
+
.ui-dropdown__panel > .ui-dropdown__head {
|
|
225
|
+
margin: calc(var(--ui-dropdown-pad) * -1) calc(var(--ui-dropdown-pad) * -1) 5px;
|
|
207
226
|
border-bottom: 1px solid var(--border);
|
|
208
227
|
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
|
209
228
|
}
|
|
229
|
+
.ui-dropdown__panel > .ui-dropdown__foot {
|
|
230
|
+
margin: 5px calc(var(--ui-dropdown-pad) * -1) calc(var(--ui-dropdown-pad) * -1);
|
|
231
|
+
border-top: 1px solid var(--border);
|
|
232
|
+
border-radius: 0 0 var(--radius-lg) var(--radius-lg);
|
|
233
|
+
}
|
|
210
234
|
|
|
211
235
|
/* Search — `search: true` pins a field above the rows and filters them as the
|
|
212
236
|
reader types. The rows move into a list of their own, so the field stays put
|
|
@@ -243,6 +267,12 @@
|
|
|
243
267
|
min-height: 38px;
|
|
244
268
|
transition: border-color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
|
|
245
269
|
}
|
|
270
|
+
/* Light sinks the field one rung less far: its panel is the only pure white the
|
|
271
|
+
kit paints, so --surface-2 falls two rungs under it rather than one. The
|
|
272
|
+
numbers are in the spec and the gate is stories/dropdown-field-ground.test.js.
|
|
273
|
+
why: docs/specification.md#elevation */
|
|
274
|
+
:root[data-theme="light"] .ui-dropdown__search-input { background: var(--bg); }
|
|
275
|
+
|
|
246
276
|
.ui-dropdown__search-input::placeholder { color: var(--muted); }
|
|
247
277
|
.ui-dropdown__search-input:hover { border-color: var(--border-strong); }
|
|
248
278
|
.ui-dropdown__search-input:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
|