@apliteni/apliteni-ui 0.130.0 → 0.132.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 +2 -1
- package/react/dist/index.d.ts +28 -1
- package/react/dist/index.js +628 -565
- package/src/components/account-nav.js +4 -7
- package/src/components/avatar.js +53 -0
- package/src/components/dropdown.js +529 -30
- package/src/components/shell.js +3 -2
- package/src/components/topbar.js +4 -3
- package/src/index.css +1 -0
- package/src/index.d.ts +40 -0
- package/src/index.js +1 -0
- package/src/inline.js +2 -0
- package/src/logic/avatar.js +28 -0
- package/src/logic/dropdown.js +22 -0
- package/src/styles/avatar.css +42 -0
- package/src/styles/dropdown.css +92 -22
- package/src/styles/layout.css +3 -6
- package/src/styles/topbar.css +6 -3
- package/src/tokens/tokens.css +23 -0
package/src/components/shell.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// why: docs/components.md#the-page-shell
|
|
5
5
|
import { topbar as productTopbar } from './topbar.js';
|
|
6
6
|
import { esc, icon, button } from './index.js';
|
|
7
|
+
import { avatar } from './avatar.js';
|
|
7
8
|
import { sidebarNav, breadcrumbs, wireNav } from './nav.js';
|
|
8
9
|
import { dropdown, wireDropdown } from './dropdown.js';
|
|
9
10
|
import { drawer, wireDrawer, closeDrawer } from './drawer.js';
|
|
@@ -67,7 +68,7 @@ const toReader = (a) => (isRecord(a) ? { name: str(a.name), email: str(a.email)
|
|
|
67
68
|
const toMenuReader = (a) => {
|
|
68
69
|
const { name, email } = toReader(a);
|
|
69
70
|
const rest = isRecord(a) && !Array.isArray(a) ? a : {};
|
|
70
|
-
const out = { ...rest, name: esc(name), email: esc(email), initials:
|
|
71
|
+
const out = { ...rest, name: esc(name), email: esc(email), initials: initials(name, email), key: email || name };
|
|
71
72
|
if (out.nav != null) out.nav = toItems(out.nav).map(toMenuTuple);
|
|
72
73
|
return out;
|
|
73
74
|
};
|
|
@@ -195,7 +196,7 @@ function settle(options) {
|
|
|
195
196
|
// inside it. why: docs/components.md#the-page-shell
|
|
196
197
|
const readerFace = (name, email, named, markOnly = false) =>
|
|
197
198
|
`<span class="ui-app__av"${named ? ` role="img" aria-label="Signed in as ${esc(named)}"` : ' aria-hidden="true"'}>`
|
|
198
|
-
+ `${
|
|
199
|
+
+ `${avatar({ key: email || name, initials: initials(name, email), size: 32 })}</span>`
|
|
199
200
|
+ (markOnly ? '' : `<span class="ui-app__who"${named ? ' aria-hidden="true"' : ''}>`
|
|
200
201
|
+ (name ? `<b>${esc(name)}</b>` : '')
|
|
201
202
|
+ (email ? `<span>${esc(email)}</span>` : '')
|
package/src/components/topbar.js
CHANGED
|
@@ -4,6 +4,7 @@ import { brand } from '../assets/brand.js';
|
|
|
4
4
|
import { icon, sun, moon } from '../assets/icons.js';
|
|
5
5
|
import { esc } from './index.js';
|
|
6
6
|
import { wireDropdown } from './dropdown.js';
|
|
7
|
+
import { avatar } from './avatar.js';
|
|
7
8
|
import { accountMenuNav, initials, toMenuTuple } from './account-nav.js';
|
|
8
9
|
import { trustedAttr, trustedUrl } from '../html.js';
|
|
9
10
|
const THEME_KEY = 'apliteni-strategy-theme';
|
|
@@ -55,7 +56,7 @@ export function versionSwitcher(versions = [], activeIdx = 0) {
|
|
|
55
56
|
// shell.js computes the mark from the caller's own strings and passes it down
|
|
56
57
|
// beside them. Left out, it is computed here from `name` and `email`.
|
|
57
58
|
export function accountMenu({
|
|
58
|
-
name = 'Ada Lovelace', email = 'ada@apliteni.com', active = 'prefs', nav, initials: mark,
|
|
59
|
+
name = 'Ada Lovelace', email = 'ada@apliteni.com', active = 'prefs', nav, initials: mark, key,
|
|
59
60
|
} = {}) {
|
|
60
61
|
// initials() is shared with the rail's avatar — the two are the same reader
|
|
61
62
|
// on the /account preset, and they used to disagree about who that was.
|
|
@@ -76,9 +77,9 @@ export function accountMenu({
|
|
|
76
77
|
// `on` so the menu is visible in Storybook / standalone use (no /auth/me gate).
|
|
77
78
|
// Consumes the shared dropdown wiring via the generic [data-dropdown] hooks.
|
|
78
79
|
return `<div class="acct on" data-dropdown>` +
|
|
79
|
-
`<button class="avatar" data-dropdown-trigger aria-haspopup="menu" aria-expanded="false" aria-label="Account">${ini}</button>` +
|
|
80
|
+
`<button class="avatar" data-dropdown-trigger aria-haspopup="menu" aria-expanded="false" aria-label="Account">${avatar({ key: key ?? (email || name), initials: ini, size: 32 })}</button>` +
|
|
80
81
|
`<div class="amenu" data-dropdown-panel role="menu">` +
|
|
81
|
-
`<div class="ahead"
|
|
82
|
+
`<div class="ahead">${avatar({ key: key ?? (email || name), initials: ini, size: 32 })}<span class="aw"><span class="anm">${name}</span><span class="aem" title="${trustedAttr(email)}">${email}</span></span></div>` +
|
|
82
83
|
items.map(it).join('') +
|
|
83
84
|
`<div class="asep"></div><a class="aout" href="#logout" data-dd-item tabindex="-1" role="menuitem">${icon('logout')}Sign out</a>` +
|
|
84
85
|
`</div></div>`;
|
package/src/index.css
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -31,6 +31,14 @@ export declare function icon(name: string, className?: string): string;
|
|
|
31
31
|
export declare function snippet(opts?: Record<string, unknown>): string;
|
|
32
32
|
export declare function button(opts?: Record<string, unknown>): string;
|
|
33
33
|
export declare function badge(label: string, variant?: string): string;
|
|
34
|
+
export declare const AVATAR_HUES: 8;
|
|
35
|
+
/** Which of the eight person hues a key gets: FNV-1a 32 bit over its UTF-8 bytes, modulo 8. */
|
|
36
|
+
export declare function avatarHue(key: unknown): number;
|
|
37
|
+
/** The first letter of the first word and of the last word, upper-cased; '' for no name. */
|
|
38
|
+
export declare function avatarInitials(name: unknown): string;
|
|
39
|
+
export declare function avatar(opts?: {
|
|
40
|
+
key?: string | number; name?: string | null; initials?: string; src?: string; size?: 24 | 32; empty?: boolean;
|
|
41
|
+
}): string;
|
|
34
42
|
export declare const calloutIcons: Record<'neutral' | 'info' | 'success' | 'warn' | 'danger', string>;
|
|
35
43
|
export declare function callout(opts?: Record<string, unknown>): string;
|
|
36
44
|
export declare function card(opts?: Record<string, unknown>): string;
|
|
@@ -87,6 +95,38 @@ export declare function dropdownHeightFit(
|
|
|
87
95
|
dd: Element | null | undefined,
|
|
88
96
|
panel: Element | null | undefined,
|
|
89
97
|
): number | null;
|
|
98
|
+
/** Stop an open panel's height where it cuts a row through the middle, so the list
|
|
99
|
+
* reads as going on: the smaller of its cap and its measured room. Call it with the
|
|
100
|
+
* panel laid out and its room written.
|
|
101
|
+
* why: docs/components.md#the-dropdown-panel */
|
|
102
|
+
export declare function dropdownSnap(
|
|
103
|
+
dd: Element | null | undefined,
|
|
104
|
+
panel: Element | null | undefined,
|
|
105
|
+
): void;
|
|
106
|
+
/** Lift an open panel out of a Modal or Drawer body too short to hold its field and
|
|
107
|
+
* the panel's floor, so it opens over the body the way a portalled panel does, or put a
|
|
108
|
+
* lifted one back. Returns whether the panel is lifted.
|
|
109
|
+
* why: docs/components.md#the-dropdown-panel */
|
|
110
|
+
export declare function dropdownLift(
|
|
111
|
+
dd: Element | null | undefined,
|
|
112
|
+
panel: Element | null | undefined,
|
|
113
|
+
): boolean;
|
|
114
|
+
/** Scroll the body an open panel sits in the least distance that shows the whole
|
|
115
|
+
* panel and its field's label, with the room under the panel kept. Returns the distance scrolled, 0 when
|
|
116
|
+
* nothing moved. why: docs/components.md#the-dropdown-panel */
|
|
117
|
+
export declare function dropdownReveal(
|
|
118
|
+
dd: Element | null | undefined,
|
|
119
|
+
panel: Element | null | undefined,
|
|
120
|
+
): number;
|
|
121
|
+
/** Give an open panel its box, and take the box away from a shut one once its fade is
|
|
122
|
+
* over, so a shut panel adds no scroll room. Returns a function that cancels a
|
|
123
|
+
* collapse still waiting. why: docs/components.md#the-dropdown-panel */
|
|
124
|
+
export declare function dropdownRest(
|
|
125
|
+
dd: Element | null | undefined,
|
|
126
|
+
panel: Element | null | undefined,
|
|
127
|
+
open: boolean,
|
|
128
|
+
wait?: number,
|
|
129
|
+
): () => void;
|
|
90
130
|
export declare function backLink(opts?: Record<string, unknown>): string;
|
|
91
131
|
export declare function statBand(opts?: Record<string, unknown>): string;
|
|
92
132
|
export declare function commandPalette(opts?: Record<string, unknown>): string;
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// apliteni-ui — public entry.
|
|
2
2
|
// Styles ship separately: `import 'apliteni-ui/css'`.
|
|
3
3
|
export * from './components/index.js';
|
|
4
|
+
export * from './components/avatar.js';
|
|
4
5
|
export * from './components/dropdown.js';
|
|
5
6
|
export * from './components/tooltip.js';
|
|
6
7
|
export * from './components/tabs.js';
|
package/src/inline.js
CHANGED
|
@@ -35,6 +35,7 @@ export const styles = {
|
|
|
35
35
|
button: read('styles/button.css'),
|
|
36
36
|
card: read('styles/card.css'),
|
|
37
37
|
badge: read('styles/badge.css'),
|
|
38
|
+
avatar: read('styles/avatar.css'),
|
|
38
39
|
segmented: read('styles/segmented.css'),
|
|
39
40
|
tabs: read('styles/tabs.css'),
|
|
40
41
|
input: read('styles/input.css'),
|
|
@@ -72,6 +73,7 @@ export const cssText = [
|
|
|
72
73
|
styles.button,
|
|
73
74
|
styles.card,
|
|
74
75
|
styles.badge,
|
|
76
|
+
styles.avatar,
|
|
75
77
|
styles.segmented,
|
|
76
78
|
styles.tabs,
|
|
77
79
|
styles.input,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** How many hues a person's mark can take. One per `--person-N` token. */
|
|
2
|
+
export const AVATAR_HUES = 8;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Which hue a person gets: FNV-1a, 32 bit, over the UTF-8 bytes of the app's stable
|
|
6
|
+
* key, modulo the number of hues. The kit ships it so two apps give one person one hue.
|
|
7
|
+
*/
|
|
8
|
+
export function avatarHue(key) {
|
|
9
|
+
let h = 0x811c9dc5;
|
|
10
|
+
for (const byte of new TextEncoder().encode(String(key ?? ''))) {
|
|
11
|
+
h ^= byte;
|
|
12
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
13
|
+
}
|
|
14
|
+
return h % AVATAR_HUES;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The letters of a mark: the first letter of the first word and of the last word, so a
|
|
19
|
+
* one-word name gives one letter. Empty when there is no name to read.
|
|
20
|
+
*/
|
|
21
|
+
export function avatarInitials(name) {
|
|
22
|
+
const words = String(name ?? '').normalize('NFC').trim().split(/\s+/).filter(Boolean);
|
|
23
|
+
if (!words.length) return '';
|
|
24
|
+
const segmenter = typeof Intl.Segmenter === 'function' ? new Intl.Segmenter(undefined, { granularity: 'grapheme' }) : null;
|
|
25
|
+
const first = (w) => segmenter ? segmenter.segment(w)[Symbol.iterator]().next().value.segment : Array.from(w)[0];
|
|
26
|
+
const letters = words.length === 1 ? [first(words[0])] : [first(words[0]), first(words[words.length - 1])];
|
|
27
|
+
return letters.join('').toLocaleUpperCase();
|
|
28
|
+
}
|
package/src/logic/dropdown.js
CHANGED
|
@@ -37,3 +37,25 @@ export const dropdownAvail = ({ anchorTop, anchorBottom, viewport, gap, inset, m
|
|
|
37
37
|
const room = Math.max(up ? anchorTop - gap : viewport - anchorBottom - gap, 0);
|
|
38
38
|
return room < min ? Infinity : Math.max(room - inset, min);
|
|
39
39
|
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Where a capped panel stops so that its last row is cut through the middle: the
|
|
43
|
+
* list then reads as going on, and a half row is the least that says so. `rows` are
|
|
44
|
+
* the rows' tops and heights from the panel's top edge, scrolled to its start;
|
|
45
|
+
* `below` is the panel's box under the part that scrolls; `content` is the panel's
|
|
46
|
+
* full height; `ceiling` is the tallest it may be. `reach`, past the ceiling, is room
|
|
47
|
+
* that may be spent to stop at the next half row instead of a whole row short: the
|
|
48
|
+
* least of it that does so is taken. Returns the height, or `null` where the panel fits
|
|
49
|
+
* whole under the ceiling or no row's middle fits under it.
|
|
50
|
+
*/
|
|
51
|
+
export const dropdownStop = ({ rows, below, content, ceiling, reach = ceiling }) => {
|
|
52
|
+
if (!(content > ceiling)) return null;
|
|
53
|
+
let stop = null;
|
|
54
|
+
let over = null;
|
|
55
|
+
for (const { top, height } of rows) {
|
|
56
|
+
const cut = Math.floor(top + height / 2 + below);
|
|
57
|
+
if (cut <= ceiling && (stop === null || cut > stop)) stop = cut;
|
|
58
|
+
else if (cut > ceiling && cut <= reach && (over === null || cut < over)) over = cut;
|
|
59
|
+
}
|
|
60
|
+
return over ?? stop;
|
|
61
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* Avatar — a person's round mark. Solid fill in the person's hue, the letters in the
|
|
2
|
+
solid-signal ink. Only the part's own paint is here: a control that holds one keeps
|
|
3
|
+
its own cursor, hover and ring. why: docs/components.md#avatar */
|
|
4
|
+
.ui-avatar {
|
|
5
|
+
--ui-avatar-size: 24px;
|
|
6
|
+
--ui-avatar-hue: var(--person-0);
|
|
7
|
+
inline-size: var(--ui-avatar-size);
|
|
8
|
+
block-size: var(--ui-avatar-size);
|
|
9
|
+
flex: none;
|
|
10
|
+
display: inline-grid;
|
|
11
|
+
place-items: center;
|
|
12
|
+
position: relative;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
border-radius: 50%;
|
|
15
|
+
font-size: var(--text-xs);
|
|
16
|
+
font-weight: var(--weight-bold);
|
|
17
|
+
line-height: 1;
|
|
18
|
+
vertical-align: middle;
|
|
19
|
+
background: var(--ui-avatar-hue); /* code-bg: inherit — an opaque fill with its own ink. */
|
|
20
|
+
color: var(--signal-solid-ink);
|
|
21
|
+
}
|
|
22
|
+
.ui-avatar--32 { --ui-avatar-size: 32px; font-size: var(--text-base); }
|
|
23
|
+
/* Two letters in 24px leave the type at the kit's smallest step; in 32px one step down. */
|
|
24
|
+
.ui-avatar--32[data-initials="2"] { font-size: var(--text-sm); }
|
|
25
|
+
.ui-avatar--h0 { --ui-avatar-hue: var(--person-0); }
|
|
26
|
+
.ui-avatar--h1 { --ui-avatar-hue: var(--person-1); }
|
|
27
|
+
.ui-avatar--h2 { --ui-avatar-hue: var(--person-2); }
|
|
28
|
+
.ui-avatar--h3 { --ui-avatar-hue: var(--person-3); }
|
|
29
|
+
.ui-avatar--h4 { --ui-avatar-hue: var(--person-4); }
|
|
30
|
+
.ui-avatar--h5 { --ui-avatar-hue: var(--person-5); }
|
|
31
|
+
.ui-avatar--h6 { --ui-avatar-hue: var(--person-6); }
|
|
32
|
+
.ui-avatar--h7 { --ui-avatar-hue: var(--person-7); }
|
|
33
|
+
|
|
34
|
+
/* The photo covers the letters, so one that fails leaves them showing. */
|
|
35
|
+
.ui-avatar img { position: absolute; inset: 0; inline-size: 100%; block-size: 100%; object-fit: cover; opacity: 0; }
|
|
36
|
+
.ui-avatar img.is-loaded { opacity: 1; }
|
|
37
|
+
.ui-avatar svg { inline-size: 62%; block-size: 62%; stroke: currentColor; fill: none; stroke-width: 2.5; }
|
|
38
|
+
|
|
39
|
+
/* No name: the user glyph on the kit's quiet fill. */
|
|
40
|
+
.ui-avatar--anon { background: var(--surface-3); /* code-bg: inherit — a glyph, no text on it. */ /* muted-ink: glyph — a person with no name */ color: var(--muted); }
|
|
41
|
+
/* No one: a dashed ring of the same size. */
|
|
42
|
+
.ui-avatar--empty { background: none; border: 1px dashed var(--muted); /* muted-ink: glyph — the ring is a mark, not text */ color: var(--muted); }
|
package/src/styles/dropdown.css
CHANGED
|
@@ -6,6 +6,22 @@
|
|
|
6
6
|
* ========================================================================== */
|
|
7
7
|
|
|
8
8
|
.ui-dropdown { position: relative; display: inline-flex; }
|
|
9
|
+
/* Inside a scrolling body the wiring writes --ui-dropdown-reach when it opens the
|
|
10
|
+
panel: the distance from the trigger to one edge inset past the panel. The panel is
|
|
11
|
+
the last thing the body holds, so the body ends where the panel ends; this 1px spot
|
|
12
|
+
lets it scroll that far. It stays through the fade and until the reader has scrolled
|
|
13
|
+
out of it, so closing does not move the field, and it paints nothing.
|
|
14
|
+
why: docs/components.md#the-dropdown-panel */
|
|
15
|
+
.ui-dropdown[style*="--ui-dropdown-reach"]::after {
|
|
16
|
+
content: "";
|
|
17
|
+
position: absolute;
|
|
18
|
+
inset-inline-start: 0;
|
|
19
|
+
top: 100%;
|
|
20
|
+
width: 1px;
|
|
21
|
+
height: 1px;
|
|
22
|
+
margin-top: var(--ui-dropdown-reach);
|
|
23
|
+
pointer-events: none;
|
|
24
|
+
}
|
|
9
25
|
|
|
10
26
|
/* Trigger — pill button: [pre] [value] [chevron] */
|
|
11
27
|
.ui-dropdown__trigger {
|
|
@@ -88,12 +104,26 @@
|
|
|
88
104
|
the screen's edge sets it to 0.
|
|
89
105
|
why: docs/components.md#the-dropdown-panel */
|
|
90
106
|
--ui-dropdown-edge: 8px;
|
|
107
|
+
/* What a panel that opens down inside a Modal or Drawer body keeps between its end
|
|
108
|
+
and the body's end. The drop shadow fades out over 43px in light and 15px in dark,
|
|
109
|
+
measured from pixels; the body clips whatever is left of it. 48px is the 8px
|
|
110
|
+
step above the light figure. why: docs/components.md#the-dropdown-panel */
|
|
111
|
+
--ui-dropdown-shade: 48px;
|
|
91
112
|
/* The least height a capped panel keeps before it would rather spend the edge
|
|
92
113
|
inset above than shrink further — three rows or so. Below it the panel
|
|
93
114
|
still ends at or before the edge; it never spends room past the edge to
|
|
94
115
|
reach this floor, because that room does not exist. #489
|
|
95
116
|
why: docs/components.md#the-dropdown-panel */
|
|
96
117
|
--ui-dropdown-min: 120px;
|
|
118
|
+
/* The tallest a panel grows before it scrolls inside itself. It is the figure the
|
|
119
|
+
reference systems settle on, 256 to 300px, and the one `scroll` and the search
|
|
120
|
+
list already used. The wiring stops the cap short of it, in the middle of a row
|
|
121
|
+
(--ui-dropdown-fit), so the last row is cut and the list is seen to go on. That
|
|
122
|
+
shows 7 whole rows and half of the eighth in a plain list, 4 and a half with a
|
|
123
|
+
description line, 5 and a half under a search field and 3 and a half with both.
|
|
124
|
+
A consumer asks for another cap with `scroll: <n>`.
|
|
125
|
+
why: docs/components.md#the-dropdown-panel */
|
|
126
|
+
--ui-dropdown-max: 300px;
|
|
97
127
|
/* Named for the same reason the offset is: a block pinned to an edge has to
|
|
98
128
|
bleed back through it, and #306 is a page copying the number by hand.
|
|
99
129
|
why: docs/components.md#the-dropdown-panel */
|
|
@@ -142,15 +172,18 @@
|
|
|
142
172
|
/* The cap, and the whole of #489: a panel took its content's height until that
|
|
143
173
|
height ran off the screen, and then it took what was left. Two numbers decide
|
|
144
174
|
it and the smaller wins — --ui-dropdown-cap is what the consumer asked for
|
|
145
|
-
(the `scroll` option writes it
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
175
|
+
(the `scroll` option writes it, and --ui-dropdown-max stands in when it is
|
|
176
|
+
unset), --ui-dropdown-avail is the room between the trigger and the viewport
|
|
177
|
+
edge, or the body's, which the wiring measures when the panel opens. Between
|
|
178
|
+
them --ui-dropdown-fit is the height the wiring stops the smaller at, in the
|
|
179
|
+
middle of a row. An unset avail
|
|
180
|
+
means nothing has measured yet, which is a server-rendered page before
|
|
181
|
+
hydration — so the fallback promises the same thing at a coarser grain, the
|
|
182
|
+
viewport less the edge inset at each end.
|
|
151
183
|
why: docs/components.md#the-dropdown-panel */
|
|
152
184
|
max-height: min(
|
|
153
|
-
var(--ui-dropdown-cap,
|
|
185
|
+
var(--ui-dropdown-cap, var(--ui-dropdown-max)),
|
|
186
|
+
var(--ui-dropdown-fit, 100dvh),
|
|
154
187
|
var(--ui-dropdown-avail, calc(100dvh - 2 * var(--ui-dropdown-edge)))
|
|
155
188
|
);
|
|
156
189
|
overflow-y: auto;
|
|
@@ -173,21 +206,40 @@
|
|
|
173
206
|
z-index: var(--z-dropdown);
|
|
174
207
|
}
|
|
175
208
|
.ui-dropdown__panel.is-end { left: auto; right: 0; }
|
|
176
|
-
/* `.is-scroll` asks for
|
|
177
|
-
the room the viewport left either way. #489 */
|
|
178
|
-
.ui-dropdown__panel.is-scroll { --ui-dropdown-cap:
|
|
209
|
+
/* `.is-scroll` asks for the standard cap by name; the panel above caps every panel
|
|
210
|
+
at the room the viewport left either way. #489 */
|
|
211
|
+
.ui-dropdown__panel.is-scroll { --ui-dropdown-cap: var(--ui-dropdown-max); }
|
|
212
|
+
|
|
213
|
+
/* The wiring adds this where the room is below the floor: even spending the whole
|
|
214
|
+
edge inset, there is no usable height. The panel keeps its cap — it is never
|
|
215
|
+
taller than 300px — and scrolls inside it. Its room runs past the fold, so the
|
|
216
|
+
page is the way to the rows below it, and `overscroll-behavior: contain` above
|
|
217
|
+
would stop the wheel at the panel's own end instead of passing it on. `auto`
|
|
218
|
+
lets it through. why: dropdownAvail(), src/logic/dropdown.js */
|
|
219
|
+
.ui-dropdown__panel.is-unbounded { overscroll-behavior: auto; }
|
|
179
220
|
|
|
180
|
-
/* The wiring
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
.ui-dropdown__panel.
|
|
221
|
+
/* The wiring lifts a panel out of a Modal or Drawer body too short to hold the field's
|
|
222
|
+
label and trigger, the floor and the 8px edge under it. Fixed, it opens over the
|
|
223
|
+
body the way a portalled panel does, and the wiring writes its edges from the
|
|
224
|
+
trigger's. The value is the side it opens on. why: docs/components.md#the-dropdown-panel */
|
|
225
|
+
.ui-dropdown__panel[data-dd-lifted] { position: fixed; }
|
|
226
|
+
.ui-dropdown__panel[data-dd-lifted="up"] { transform: translateY(6px); }
|
|
227
|
+
|
|
228
|
+
/* A search list that carries the consumer's own cap keeps it, and the panel is that cap
|
|
229
|
+
and the search field over it. The wiring writes the sum as --ui-dropdown-fit; this
|
|
230
|
+
lifts the standard cap out of its way. why: docs/components.md#the-dropdown-panel */
|
|
231
|
+
.ui-dropdown__panel--search:has(> .ui-dropdown__list[style*="--ui-dropdown-cap"]) { --ui-dropdown-max: 100dvh; }
|
|
232
|
+
|
|
233
|
+
/* A shut panel is still laid out, and a box that is laid out counts towards the
|
|
234
|
+
scroll of the body or page it sits in. The wiring collapses it once its fade is
|
|
235
|
+
over and gives the box back before the next open measures it. The width stays, so
|
|
236
|
+
the horizontal fit reads the same number. why: docs/components.md#the-dropdown-panel */
|
|
237
|
+
.ui-dropdown__panel[data-dd-shut] {
|
|
238
|
+
max-height: 0;
|
|
239
|
+
padding-block: 0;
|
|
240
|
+
border-block-width: 0;
|
|
241
|
+
overflow: hidden;
|
|
242
|
+
}
|
|
191
243
|
|
|
192
244
|
/* Chrome makes a scroll container a keyboard stop on its own — no tabindex, no
|
|
193
245
|
author rule — so `.is-scroll` took the browser's outline, black in light mode.
|
|
@@ -396,7 +448,7 @@
|
|
|
396
448
|
|
|
397
449
|
/* The rows' own cap, read from the same property the panel reads: the `scroll`
|
|
398
450
|
option lands on the list when there is a field above it. #489 */
|
|
399
|
-
.ui-dropdown__list { max-height: var(--ui-dropdown-cap, 300px); overflow-y: auto; overscroll-behavior: contain; }
|
|
451
|
+
.ui-dropdown__list { max-height: var(--ui-dropdown-cap, var(--ui-dropdown-fit, var(--ui-dropdown-max, 300px))); overflow-y: auto; overscroll-behavior: contain; }
|
|
400
452
|
/* Rows are `role="option" tabindex="-1"`, so the LIST takes the keyboard stop and
|
|
401
453
|
answered with the native outline. The band is drawn INWARD, on the list: an outset
|
|
402
454
|
one 6px inside a --radius-lg corner leaves the panel, and the panel is not the box
|
|
@@ -425,3 +477,21 @@
|
|
|
425
477
|
/* muted-ink: state — off, unset, disabled or archived badge */
|
|
426
478
|
color: var(--muted);
|
|
427
479
|
}
|
|
480
|
+
|
|
481
|
+
/* A picker of people. A mark leads the trigger and each row; with no value the trigger
|
|
482
|
+
shows the dashed ring beside the placeholder in the placeholder ink. The mark's own
|
|
483
|
+
margins give back the room it takes, so the trigger keeps its height.
|
|
484
|
+
why: docs/components.md#avatar */
|
|
485
|
+
.ui-dropdown__trigger > .ui-avatar {
|
|
486
|
+
--ui-avatar-pull: 4px;
|
|
487
|
+
--ui-avatar-pull-start: 10px;
|
|
488
|
+
margin-block: calc(var(--ui-avatar-pull) * -1);
|
|
489
|
+
margin-inline-start: calc(var(--ui-avatar-pull-start) * -1);
|
|
490
|
+
}
|
|
491
|
+
.ui-dropdown__value.is-placeholder { /* muted-ink: placeholder — no person chosen */ color: var(--muted); font-weight: var(--weight-medium); }
|
|
492
|
+
|
|
493
|
+
/* A dropdown that is a form control takes its field's width, and its panel the
|
|
494
|
+
trigger's. why: docs/components.md#avatar */
|
|
495
|
+
.ui-field > .ui-dropdown { display: grid; }
|
|
496
|
+
.ui-field > .ui-dropdown > .ui-dropdown__trigger > .ui-dropdown__chevron { margin-left: auto; }
|
|
497
|
+
.ui-field > .ui-dropdown > .ui-dropdown__panel { inline-size: 100%; min-width: 0; }
|
package/src/styles/layout.css
CHANGED
|
@@ -161,13 +161,10 @@
|
|
|
161
161
|
.ui-app__av {
|
|
162
162
|
width: var(--ui-app-av); height: var(--ui-app-av); flex: none;
|
|
163
163
|
margin-inline-start: calc((var(--ui-nav-strip) - var(--ui-app-av)) / 2);
|
|
164
|
-
|
|
165
|
-
background: var(--surface); /* code-bg: inherit — decorative or text-only paint; nothing nested reads a ground from it. */
|
|
166
|
-
color: var(--strong);
|
|
167
|
-
display: grid; place-items: center;
|
|
168
|
-
font-size: calc(var(--text-xs) + 1px);
|
|
169
|
-
font-weight: var(--weight-semibold);
|
|
164
|
+
display: grid;
|
|
170
165
|
}
|
|
166
|
+
/* The mark is the Avatar part in the reader's hue, filling this box like the top bar's. */
|
|
167
|
+
.ui-app__av > .ui-avatar { --ui-avatar-size: 100%; }
|
|
171
168
|
.ui-app__who { font-size: calc(var(--text-sm) - 0.5px); line-height: 1.3; min-width: 0; }
|
|
172
169
|
.ui-app__who b { display: block; color: var(--strong); font-weight: var(--weight-semibold); }
|
|
173
170
|
/* nowrap is what makes the ellipsis fire — without it the address wraps, and
|
package/src/styles/topbar.css
CHANGED
|
@@ -206,10 +206,10 @@
|
|
|
206
206
|
transition: background var(--dur-fast) var(--ease);
|
|
207
207
|
}
|
|
208
208
|
/* Menu rows use the reading surface and an edge on hover — see
|
|
209
|
-
src/styles/dropdown.css:
|
|
209
|
+
src/styles/dropdown.css:323 `.ui-dropdown__item:hover`.
|
|
210
210
|
why: docs/foundations.md#elevation */
|
|
211
211
|
.vopt:hover { outline: 1px solid var(--control-edge); outline-offset: -1px; background: var(--surface); /* code-bg: inherit — control paint; the containing surface is the ground, for a chip and for a band alike. */ }
|
|
212
|
-
/* The row is a keyboard stop: src/components/dropdown.js:
|
|
212
|
+
/* The row is a keyboard stop: src/components/dropdown.js:1663
|
|
213
213
|
`items[next < 0 ? 0 : next]?.focus()` moves focus onto a `[data-dd-item]` with
|
|
214
214
|
the arrow keys, and this is one. It takes the ring on its own 10px radius, the
|
|
215
215
|
same way `.amenu a` above does. #482 */
|
|
@@ -306,6 +306,10 @@
|
|
|
306
306
|
what the kit's rows already chose. why: docs/foundations.md#the-focus-ring */
|
|
307
307
|
.acct .avatar:hover:not(:focus-visible) { border-color: var(--accent); }
|
|
308
308
|
.avatar:focus-visible { outline: var(--ring); outline-offset: var(--ring-offset); }
|
|
309
|
+
/* The account button holding an Avatar: the part paints the circle in the reader's hue and
|
|
310
|
+
the button keeps its edge, cursor, hover and ring. why: docs/components.md#avatar */
|
|
311
|
+
.avatar:has(> .ui-avatar) { background: none; padding: 0; overflow: hidden; }
|
|
312
|
+
.avatar > .ui-avatar { --ui-avatar-size: 100%; }
|
|
309
313
|
.amenu {
|
|
310
314
|
position: absolute;
|
|
311
315
|
right: 0; top: 44px;
|
|
@@ -372,7 +376,6 @@
|
|
|
372
376
|
border-radius: 14px 14px 0 0;
|
|
373
377
|
display: flex; align-items: center; gap: 11px;
|
|
374
378
|
}
|
|
375
|
-
.amenu .ahead .avatar { cursor: default; }
|
|
376
379
|
.amenu .ahead .aw { min-width: 0; display: flex; flex-direction: column; gap: 1px; line-height: 1.35; }
|
|
377
380
|
.amenu .ahead .anm { color: var(--strong); font-size: var(--text-sm); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
378
381
|
.amenu .ahead .aem { color: var(--text); font-size: calc(var(--text-xs) + 0.5px); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
package/src/tokens/tokens.css
CHANGED
|
@@ -257,6 +257,19 @@
|
|
|
257
257
|
--signal-solid-info: var(--cyan);
|
|
258
258
|
--signal-solid-neutral: var(--muted);
|
|
259
259
|
|
|
260
|
+
/* Person hues: the eight fills an Avatar takes from a hash of the person's key. Fixed
|
|
261
|
+
values and not accent tints, so a person keeps one hue under every accent and in
|
|
262
|
+
every app. Each is an existing kit value for this theme, and --signal-solid-ink
|
|
263
|
+
holds 4.5:1 on all eight. why: docs/components.md#avatar */
|
|
264
|
+
--person-0: #bd8cff;
|
|
265
|
+
--person-1: #5ab0ff;
|
|
266
|
+
--person-2: #20dcf5;
|
|
267
|
+
--person-3: #3ad9a0;
|
|
268
|
+
--person-4: #98ff8f;
|
|
269
|
+
--person-5: #ffcf6a;
|
|
270
|
+
--person-6: #ff8a5c;
|
|
271
|
+
--person-7: #f5aec6;
|
|
272
|
+
|
|
260
273
|
/* DEPRECATED since #295: no surface below the floating step casts, and the floating
|
|
261
274
|
step reads --elev-drop rather than any of these. Published still, so a consumer
|
|
262
275
|
reading one gets nothing rather than an unresolved var(), and a consumer who wants a
|
|
@@ -446,6 +459,16 @@
|
|
|
446
459
|
--signal-solid-danger: var(--chip-danger-ink);
|
|
447
460
|
--signal-solid-warn: var(--chip-warn-ink);
|
|
448
461
|
--signal-solid-info: var(--chip-info-ink);
|
|
462
|
+
|
|
463
|
+
/* Person hues, light: the deepened forms, so white holds on every one. */
|
|
464
|
+
--person-0: #6a2dcc;
|
|
465
|
+
--person-1: #005ab4;
|
|
466
|
+
--person-2: #096a7c;
|
|
467
|
+
--person-3: #076c48;
|
|
468
|
+
--person-4: #145a26;
|
|
469
|
+
--person-5: #825900;
|
|
470
|
+
--person-6: #a8370c;
|
|
471
|
+
--person-7: #b7295f;
|
|
449
472
|
--signal-solid-neutral: var(--muted);
|
|
450
473
|
|
|
451
474
|
/* Deprecated and unread — see the dark block. */
|