@mixtint/primer-view-components 0.78.0 → 0.84.5
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 +20 -1
- package/app/assets/javascripts/components/primer/alpha/tree_view/tree_view.d.ts +1 -0
- package/app/assets/javascripts/components/primer/open_project/avatar_fallback.d.ts +28 -0
- package/app/assets/javascripts/components/primer/primer.d.ts +2 -0
- package/app/assets/javascripts/lib/primer/forms/character_counter.d.ts +41 -0
- package/app/assets/javascripts/lib/primer/forms/primer_text_area.d.ts +13 -0
- package/app/assets/javascripts/lib/primer/forms/primer_text_field.d.ts +2 -0
- package/app/assets/javascripts/primer_view_components.js +1 -1
- package/app/assets/javascripts/primer_view_components.js.map +1 -1
- package/app/assets/styles/primer_view_components.css +343 -22
- package/app/assets/styles/primer_view_components.css.map +1 -1
- package/app/components/primer/alpha/action_bar_element.js +68 -77
- package/app/components/primer/alpha/action_list.css +1 -1
- package/app/components/primer/alpha/action_list.js +1 -1
- package/app/components/primer/alpha/action_menu/action_menu_element.js +5 -1
- package/app/components/primer/alpha/dialog.css +12 -0
- package/app/components/primer/alpha/dialog.css.json +2 -1
- package/app/components/primer/alpha/segmented_control.js +1 -1
- package/app/components/primer/alpha/select_panel_element.js +4 -2
- package/app/components/primer/alpha/tab_nav.css +8 -1
- package/app/components/primer/alpha/tab_nav.css.json +1 -0
- package/app/components/primer/alpha/toggle_switch.js +1 -1
- package/app/components/primer/alpha/tool_tip.js +13 -6
- package/app/components/primer/alpha/tree_view/tree_view.d.ts +1 -0
- package/app/components/primer/alpha/tree_view/tree_view.js +24 -17
- package/app/components/primer/alpha/tree_view/tree_view_icon_pair_element.js +1 -1
- package/app/components/primer/alpha/tree_view/tree_view_include_fragment_element.js +1 -1
- package/app/components/primer/alpha/tree_view/tree_view_sub_tree_node_element.js +25 -2
- package/app/components/primer/alpha/x_banner.js +1 -1
- package/app/components/primer/beta/avatar_stack.css +21 -31
- package/app/components/primer/beta/avatar_stack.css.json +7 -4
- package/app/components/primer/beta/blankslate.css +7 -0
- package/app/components/primer/beta/blankslate.css.json +1 -0
- package/app/components/primer/beta/details_toggle_element.js +1 -1
- package/app/components/primer/beta/nav_list.js +1 -1
- package/app/components/primer/beta/nav_list_group_element.js +1 -1
- package/app/components/primer/beta/state.css +1 -2
- package/app/components/primer/beta/timeline_item.css +2 -1
- package/app/components/primer/dialog_helper.js +24 -9
- package/app/components/primer/open_project/avatar_fallback.d.ts +28 -0
- package/app/components/primer/open_project/avatar_fallback.js +105 -0
- package/app/components/primer/open_project/avatar_stack.css +29 -0
- package/app/components/primer/open_project/avatar_stack.css.json +10 -0
- package/app/components/primer/open_project/border_box/collapsible_header.css +72 -10
- package/app/components/primer/open_project/border_box/collapsible_header.css.json +9 -2
- package/app/components/primer/open_project/inline_message.css +42 -0
- package/app/components/primer/open_project/inline_message.css.json +13 -0
- package/app/components/primer/open_project/page_header.css +40 -3
- package/app/components/primer/open_project/page_header.css.json +8 -1
- package/app/components/primer/open_project/pagination.css +87 -0
- package/app/components/primer/open_project/pagination.css.json +24 -0
- package/app/components/primer/primer.d.ts +2 -0
- package/app/components/primer/primer.js +2 -0
- package/app/components/primer/scrollable_region.js +1 -1
- package/app/lib/primer/forms/character_counter.d.ts +41 -0
- package/app/lib/primer/forms/character_counter.js +114 -0
- package/app/lib/primer/forms/primer_multi_input.js +1 -1
- package/app/lib/primer/forms/primer_text_area.d.ts +13 -0
- package/app/lib/primer/forms/primer_text_area.js +53 -0
- package/app/lib/primer/forms/primer_text_field.d.ts +2 -0
- package/app/lib/primer/forms/primer_text_field.js +17 -3
- package/app/lib/primer/forms/toggle_switch_input.js +1 -1
- package/package.json +23 -22
- package/static/arguments.json +234 -2
- package/static/audited_at.json +6 -0
- package/static/classes.json +27 -2
- package/static/constants.json +66 -1
- package/static/form_previews.json +15 -0
- package/static/info_arch.json +821 -82
- package/static/previews.json +432 -9
- package/static/statuses.json +6 -0
|
@@ -1,33 +1,30 @@
|
|
|
1
|
+
/* stylelint-disable selector-max-specificity */
|
|
2
|
+
/* The selector-max-specificity rule is disabled here because the nested selectors
|
|
3
|
+
in AvatarStack require high specificity to properly override default styles and
|
|
4
|
+
achieve the intended visual stacking. */
|
|
1
5
|
/* AvatarStack */
|
|
2
|
-
|
|
3
6
|
/* Stacked avatars can be used to show who is participating in thread when
|
|
4
7
|
** there is limited space available. */
|
|
5
|
-
|
|
6
8
|
.AvatarStack {
|
|
7
9
|
position: relative;
|
|
8
10
|
min-width: 26px;
|
|
9
11
|
height: 20px;
|
|
10
12
|
}
|
|
11
|
-
|
|
12
13
|
.AvatarStack .AvatarStack-body {
|
|
13
14
|
position: absolute;
|
|
14
15
|
}
|
|
15
|
-
|
|
16
16
|
.AvatarStack.AvatarStack--two {
|
|
17
17
|
min-width: 36px;
|
|
18
18
|
}
|
|
19
|
-
|
|
20
19
|
.AvatarStack.AvatarStack--three-plus {
|
|
21
20
|
min-width: 46px;
|
|
22
21
|
}
|
|
23
|
-
|
|
24
22
|
.AvatarStack-body {
|
|
25
23
|
display: flex;
|
|
26
24
|
background: var(--bgColor-default);
|
|
27
25
|
/* stylelint-disable-next-line primer/borders */
|
|
28
26
|
border-radius: 100px;
|
|
29
27
|
}
|
|
30
|
-
|
|
31
28
|
.AvatarStack-body .avatar {
|
|
32
29
|
position: relative;
|
|
33
30
|
z-index: 2;
|
|
@@ -41,47 +38,49 @@
|
|
|
41
38
|
border-radius: var(--borderRadius-small);
|
|
42
39
|
transition: margin 0.1s ease-in-out;
|
|
43
40
|
}
|
|
44
|
-
|
|
45
41
|
:is(.AvatarStack-body .avatar):first-child {
|
|
46
42
|
z-index: 3;
|
|
47
43
|
}
|
|
48
|
-
|
|
49
44
|
:is(.AvatarStack-body .avatar):last-child {
|
|
50
45
|
z-index: 1;
|
|
51
46
|
}
|
|
52
|
-
|
|
53
47
|
/* stylelint-disable-next-line selector-max-type */
|
|
54
|
-
|
|
55
48
|
:is(.AvatarStack-body .avatar) img {
|
|
56
49
|
border-radius: var(--borderRadius-small);
|
|
57
50
|
}
|
|
58
|
-
|
|
59
51
|
/* Account for 4+ avatars */
|
|
60
|
-
|
|
61
52
|
:is(.AvatarStack-body .avatar):nth-child(n + 4) {
|
|
62
53
|
display: none;
|
|
63
54
|
opacity: 0;
|
|
64
55
|
}
|
|
65
|
-
|
|
66
|
-
.AvatarStack-body:
|
|
56
|
+
/* stylelint-disable-next-line selector-max-type */
|
|
57
|
+
.AvatarStack-body span:nth-child(n + 4) .avatar {
|
|
58
|
+
display: none;
|
|
59
|
+
opacity: 0;
|
|
60
|
+
}
|
|
61
|
+
:is(.AvatarStack-body:hover:not([data-disable-expand]),.AvatarStack-body:focus-within:not([data-disable-expand])) .avatar {
|
|
67
62
|
margin-right: var(--base-size-4);
|
|
68
63
|
}
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
:is(.AvatarStack-body:hover:not([data-disable-expand]),.AvatarStack-body:focus-within:not([data-disable-expand])) .avatar:nth-child(n + 4) {
|
|
65
|
+
display: flex;
|
|
66
|
+
opacity: 1;
|
|
67
|
+
}
|
|
68
|
+
/* stylelint-disable-next-line selector-max-type */
|
|
69
|
+
:is(.AvatarStack-body:hover:not([data-disable-expand]),.AvatarStack-body:focus-within:not([data-disable-expand])) span:nth-child(n + 4) .avatar {
|
|
71
70
|
display: flex;
|
|
72
71
|
opacity: 1;
|
|
73
72
|
}
|
|
74
|
-
|
|
75
|
-
.AvatarStack-body:hover .avatar-more {
|
|
73
|
+
:is(.AvatarStack-body:hover:not([data-disable-expand]),.AvatarStack-body:focus-within:not([data-disable-expand])) .avatar-more {
|
|
76
74
|
display: none !important;
|
|
77
75
|
}
|
|
78
|
-
|
|
76
|
+
.AvatarStack-body[data-disable-expand] {
|
|
77
|
+
position: relative;
|
|
78
|
+
}
|
|
79
79
|
.avatar.avatar-more {
|
|
80
80
|
z-index: 1;
|
|
81
81
|
margin-right: 0;
|
|
82
82
|
background: var(--bgColor-muted);
|
|
83
83
|
}
|
|
84
|
-
|
|
85
84
|
.avatar.avatar-more::before,.avatar.avatar-more::after {
|
|
86
85
|
position: absolute;
|
|
87
86
|
display: block;
|
|
@@ -91,42 +90,33 @@
|
|
|
91
90
|
border-radius: 2px;
|
|
92
91
|
outline: var(--borderWidth-thin) solid var(--bgColor-default);
|
|
93
92
|
}
|
|
94
|
-
|
|
95
93
|
.avatar.avatar-more::before {
|
|
96
94
|
width: 17px;
|
|
97
95
|
background: var(--avatarStack-fade-bgColor-muted);
|
|
98
96
|
}
|
|
99
|
-
|
|
100
97
|
.avatar.avatar-more::after {
|
|
101
98
|
width: 14px;
|
|
102
99
|
background: var(--avatarStack-fade-bgColor-default);
|
|
103
100
|
}
|
|
104
|
-
|
|
105
101
|
/* Right aligned variation */
|
|
106
|
-
|
|
107
102
|
.AvatarStack--right .AvatarStack-body {
|
|
108
103
|
right: 0;
|
|
109
104
|
flex-direction: row-reverse;
|
|
110
105
|
}
|
|
111
|
-
|
|
112
|
-
:is(.AvatarStack--right .AvatarStack-body):hover .avatar {
|
|
106
|
+
:is(.AvatarStack--right .AvatarStack-body):hover:not([data-disable-expand]) .avatar {
|
|
113
107
|
margin-right: 0;
|
|
114
108
|
margin-left: var(--base-size-4);
|
|
115
109
|
}
|
|
116
|
-
|
|
117
110
|
.AvatarStack--right .avatar.avatar-more {
|
|
118
111
|
background: var(--avatarStack-fade-bgColor-default);
|
|
119
112
|
}
|
|
120
|
-
|
|
121
113
|
:is(.AvatarStack--right .avatar.avatar-more)::before {
|
|
122
114
|
width: 5px;
|
|
123
115
|
}
|
|
124
|
-
|
|
125
116
|
:is(.AvatarStack--right .avatar.avatar-more)::after {
|
|
126
117
|
width: 2px;
|
|
127
118
|
background: var(--bgColor-muted);
|
|
128
119
|
}
|
|
129
|
-
|
|
130
120
|
.AvatarStack--right .avatar {
|
|
131
121
|
margin-right: 0;
|
|
132
122
|
/* stylelint-disable-next-line primer/spacing */
|
|
@@ -11,14 +11,17 @@
|
|
|
11
11
|
":is(.AvatarStack-body .avatar):last-child",
|
|
12
12
|
":is(.AvatarStack-body .avatar) img",
|
|
13
13
|
":is(.AvatarStack-body .avatar):nth-child(n + 4)",
|
|
14
|
-
".AvatarStack-body:
|
|
15
|
-
".AvatarStack-body:hover
|
|
16
|
-
".AvatarStack-body:hover .avatar-
|
|
14
|
+
".AvatarStack-body span:nth-child(n + 4) .avatar",
|
|
15
|
+
":is(.AvatarStack-body:hover:not([data-disable-expand]),.AvatarStack-body:focus-within:not([data-disable-expand])) .avatar",
|
|
16
|
+
":is(.AvatarStack-body:hover:not([data-disable-expand]),.AvatarStack-body:focus-within:not([data-disable-expand])) .avatar:nth-child(n + 4)",
|
|
17
|
+
":is(.AvatarStack-body:hover:not([data-disable-expand]),.AvatarStack-body:focus-within:not([data-disable-expand])) span:nth-child(n + 4) .avatar",
|
|
18
|
+
":is(.AvatarStack-body:hover:not([data-disable-expand]),.AvatarStack-body:focus-within:not([data-disable-expand])) .avatar-more",
|
|
19
|
+
".AvatarStack-body[data-disable-expand]",
|
|
17
20
|
".avatar.avatar-more",
|
|
18
21
|
".avatar.avatar-more::before",
|
|
19
22
|
".avatar.avatar-more::after",
|
|
20
23
|
".AvatarStack--right .AvatarStack-body",
|
|
21
|
-
":is(.AvatarStack--right .AvatarStack-body):hover .avatar",
|
|
24
|
+
":is(.AvatarStack--right .AvatarStack-body):hover:not([data-disable-expand]) .avatar",
|
|
22
25
|
".AvatarStack--right .avatar.avatar-more",
|
|
23
26
|
":is(.AvatarStack--right .avatar.avatar-more)::before",
|
|
24
27
|
":is(.AvatarStack--right .avatar.avatar-more)::after",
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
.blankslate p {
|
|
21
21
|
font-size: var(--text-body-size-large);
|
|
22
22
|
color: var(--fgColor-muted);
|
|
23
|
+
max-width: 56rem;
|
|
24
|
+
margin: auto;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/* stylelint-disable-next-line selector-max-type */
|
|
@@ -58,6 +60,11 @@
|
|
|
58
60
|
margin-bottom: var(--base-size-4);
|
|
59
61
|
font-size: var(--text-title-size-medium);
|
|
60
62
|
font-weight: var(--text-title-weight-medium);
|
|
63
|
+
text-wrap: balance;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.blankslate-description {
|
|
67
|
+
text-wrap: balance;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
.blankslate-action {
|
|
@@ -105,7 +105,7 @@ __decorate([
|
|
|
105
105
|
targets
|
|
106
106
|
], NavListGroupElement.prototype, "focusMarkers", void 0);
|
|
107
107
|
NavListGroupElement = __decorate([
|
|
108
|
-
controller
|
|
108
|
+
controller('nav-list-group')
|
|
109
109
|
], NavListGroupElement);
|
|
110
110
|
export { NavListGroupElement };
|
|
111
111
|
window.NavListGroupElement = NavListGroupElement;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
font-size: var(--text-body-size-medium);
|
|
11
11
|
font-weight: var(--base-text-weight-medium);
|
|
12
12
|
/* stylelint-disable-next-line primer/typography */
|
|
13
|
-
line-height: var(--
|
|
13
|
+
line-height: var(--base-size-20);
|
|
14
14
|
text-align: center;
|
|
15
15
|
white-space: nowrap;
|
|
16
16
|
/* stylelint-disable-next-line primer/borders */
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
.State,
|
|
22
22
|
.State--draft {
|
|
23
23
|
color: var(--fgColor-onEmphasis);
|
|
24
|
-
/* stylelint-disable-next-line primer/colors */
|
|
25
24
|
background-color: var(--bgColor-draft-emphasis, var(--bgColor-neutral-emphasis));
|
|
26
25
|
border: var(--borderWidth-thin) solid transparent;
|
|
27
26
|
box-shadow: var(--boxShadow-thin) var(--borderColor-draft-emphasis, var(--borderColor-neutral-emphasis));
|
|
@@ -10,6 +10,11 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
10
10
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
11
|
};
|
|
12
12
|
var _DialogHelperElement_instances, _DialogHelperElement_abortController, _DialogHelperElement_handleDialogOpenAttribute;
|
|
13
|
+
function setScrollGutter(doc) {
|
|
14
|
+
if (doc.body.style.getPropertyValue('--dialog-scrollgutter'))
|
|
15
|
+
return;
|
|
16
|
+
doc.body.style.setProperty('--dialog-scrollgutter', `${window.innerWidth - doc.body.clientWidth}px`);
|
|
17
|
+
}
|
|
13
18
|
function dialogInvokerButtonHandler(event) {
|
|
14
19
|
const target = event.target;
|
|
15
20
|
const button = target?.closest('button');
|
|
@@ -20,25 +25,35 @@ function dialogInvokerButtonHandler(event) {
|
|
|
20
25
|
if (dialogId) {
|
|
21
26
|
const dialog = document.getElementById(dialogId);
|
|
22
27
|
if (dialog instanceof HTMLDialogElement) {
|
|
28
|
+
setScrollGutter(dialog.ownerDocument);
|
|
23
29
|
dialog.showModal();
|
|
24
30
|
// A buttons default behaviour in some browsers it to send a pointer event
|
|
25
31
|
// If the behaviour is allowed through the dialog will be shown but then
|
|
26
32
|
// quickly hidden- as if it were never shown. This prevents that.
|
|
27
33
|
event.preventDefault();
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
34
|
+
// When a <dialog> is opened with showModal() from inside a popover with popover="auto",
|
|
35
|
+
// there are two related issues:
|
|
36
|
+
//
|
|
37
|
+
// 1. In older browsers (e.g. Chrome 122), the "hide all popovers" algorithm runs when a
|
|
38
|
+
// top layer element opens, closing any ancestor popover. We must re-open those popovers.
|
|
39
|
+
// See https://github.com/whatwg/html/issues/9998,
|
|
40
|
+
// fixed by https://github.com/whatwg/html/pull/10116.
|
|
41
|
+
//
|
|
42
|
+
// 2. In newer browsers where the popover stays open, the popover="auto" behavior still
|
|
43
|
+
// interferes with the native <dialog> focus trap, causing focus to escape the dialog
|
|
44
|
+
// when tabbing past the last focusable element. Converting the popover to "manual"
|
|
45
|
+
// prevents this interference.
|
|
46
|
+
//
|
|
47
|
+
// In both cases, the fix is the same: convert ancestor auto popovers to manual.
|
|
35
48
|
let node = button;
|
|
36
49
|
let fixed = false;
|
|
37
50
|
while (node) {
|
|
38
|
-
node = node.parentElement?.closest('[popover]
|
|
51
|
+
node = node.parentElement?.closest('[popover]');
|
|
39
52
|
if (node && node.popover === 'auto') {
|
|
40
53
|
node.classList.add('dialog-inside-popover-fix');
|
|
41
54
|
node.popover = 'manual';
|
|
55
|
+
// Changing popover from "auto" to "manual" closes the popover,
|
|
56
|
+
// so we need to re-show it regardless of whether it was previously open.
|
|
42
57
|
node.showPopover();
|
|
43
58
|
fixed = true;
|
|
44
59
|
}
|
|
@@ -83,7 +98,6 @@ export class DialogHelperElement extends HTMLElement {
|
|
|
83
98
|
const { signal } = (__classPrivateFieldSet(this, _DialogHelperElement_abortController, new AbortController(), "f"));
|
|
84
99
|
document.addEventListener('click', dialogInvokerButtonHandler, true);
|
|
85
100
|
document.addEventListener('click', this, { signal });
|
|
86
|
-
this.ownerDocument.body.style.setProperty('--dialog-scrollgutter', `${window.innerWidth - this.ownerDocument.body.clientWidth}px`);
|
|
87
101
|
new MutationObserver(records => {
|
|
88
102
|
for (const record of records) {
|
|
89
103
|
if (record.target === this.dialog) {
|
|
@@ -123,6 +137,7 @@ _DialogHelperElement_abortController = new WeakMap(), _DialogHelperElement_insta
|
|
|
123
137
|
// eslint-disable-next-line no-restricted-syntax
|
|
124
138
|
this.dialog.addEventListener('close', e => e.stopImmediatePropagation(), { once: true });
|
|
125
139
|
this.dialog.close();
|
|
140
|
+
setScrollGutter(this.dialog.ownerDocument);
|
|
126
141
|
this.dialog.showModal();
|
|
127
142
|
}
|
|
128
143
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AvatarFallbackElement implements "fallback first" loading pattern:
|
|
3
|
+
* 1. Fallback SVG is rendered immediately as <img> src
|
|
4
|
+
* 2. Real avatar URL is test-loaded in background using new Image()
|
|
5
|
+
* 3. On success, swaps to real image; on failure, fallback stays visible
|
|
6
|
+
*
|
|
7
|
+
* This approach prevents flicker by never showing a broken image state.
|
|
8
|
+
* Inspired by OpenProject's Angular PrincipalRendererService.
|
|
9
|
+
*
|
|
10
|
+
* Note: We read attributes directly via getAttribute() instead of using @attr
|
|
11
|
+
* due to a Catalyst bug where @attr accessors aren't properly initialized
|
|
12
|
+
* when elements have pre-existing attribute values.
|
|
13
|
+
*/
|
|
14
|
+
export declare class AvatarFallbackElement extends HTMLElement {
|
|
15
|
+
private img;
|
|
16
|
+
private testImage;
|
|
17
|
+
connectedCallback(): void;
|
|
18
|
+
disconnectedCallback(): void;
|
|
19
|
+
/**
|
|
20
|
+
* Test-loads the real avatar URL in background.
|
|
21
|
+
* On success, swaps the visible img to the real URL.
|
|
22
|
+
* On failure, does nothing - fallback stays visible.
|
|
23
|
+
*/
|
|
24
|
+
private testLoadImage;
|
|
25
|
+
private applyColor;
|
|
26
|
+
private valueHash;
|
|
27
|
+
private updateSvgColor;
|
|
28
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { controller } from '@github/catalyst';
|
|
8
|
+
/**
|
|
9
|
+
* AvatarFallbackElement implements "fallback first" loading pattern:
|
|
10
|
+
* 1. Fallback SVG is rendered immediately as <img> src
|
|
11
|
+
* 2. Real avatar URL is test-loaded in background using new Image()
|
|
12
|
+
* 3. On success, swaps to real image; on failure, fallback stays visible
|
|
13
|
+
*
|
|
14
|
+
* This approach prevents flicker by never showing a broken image state.
|
|
15
|
+
* Inspired by OpenProject's Angular PrincipalRendererService.
|
|
16
|
+
*
|
|
17
|
+
* Note: We read attributes directly via getAttribute() instead of using @attr
|
|
18
|
+
* due to a Catalyst bug where @attr accessors aren't properly initialized
|
|
19
|
+
* when elements have pre-existing attribute values.
|
|
20
|
+
*/
|
|
21
|
+
let AvatarFallbackElement = class AvatarFallbackElement extends HTMLElement {
|
|
22
|
+
constructor() {
|
|
23
|
+
super(...arguments);
|
|
24
|
+
this.img = null;
|
|
25
|
+
this.testImage = null;
|
|
26
|
+
}
|
|
27
|
+
connectedCallback() {
|
|
28
|
+
this.img = this.querySelector('img') ?? null;
|
|
29
|
+
if (!this.img)
|
|
30
|
+
return;
|
|
31
|
+
const uniqueId = this.getAttribute('data-unique-id') || '';
|
|
32
|
+
const altText = this.getAttribute('data-alt-text') || '';
|
|
33
|
+
const avatarSrc = this.getAttribute('data-avatar-src') || '';
|
|
34
|
+
// Apply hashed color to fallback SVG immediately
|
|
35
|
+
this.applyColor(this.img, uniqueId, altText);
|
|
36
|
+
// Test-load real avatar URL in background
|
|
37
|
+
if (avatarSrc) {
|
|
38
|
+
this.testLoadImage(avatarSrc);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
disconnectedCallback() {
|
|
42
|
+
// Clean up test image and its event handler to prevent memory leaks
|
|
43
|
+
if (this.testImage) {
|
|
44
|
+
this.testImage.onload = null;
|
|
45
|
+
this.testImage = null;
|
|
46
|
+
}
|
|
47
|
+
this.img = null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Test-loads the real avatar URL in background.
|
|
51
|
+
* On success, swaps the visible img to the real URL.
|
|
52
|
+
* On failure, does nothing - fallback stays visible.
|
|
53
|
+
*/
|
|
54
|
+
testLoadImage(url) {
|
|
55
|
+
this.testImage = new Image();
|
|
56
|
+
this.testImage.onload = () => {
|
|
57
|
+
// Success - swap to real image
|
|
58
|
+
if (this.img) {
|
|
59
|
+
this.img.src = url;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
// On error: do nothing, fallback stays visible (no flicker)
|
|
63
|
+
this.testImage.src = url;
|
|
64
|
+
}
|
|
65
|
+
applyColor(img, uniqueId, altText) {
|
|
66
|
+
// If either uniqueId or altText is missing, skip color customization so the SVG
|
|
67
|
+
// keeps its default gray fill defined in the source and no color override is applied.
|
|
68
|
+
if (!uniqueId || !altText)
|
|
69
|
+
return;
|
|
70
|
+
const text = `${uniqueId}${altText}`;
|
|
71
|
+
const hue = this.valueHash(text);
|
|
72
|
+
const color = `hsl(${hue}, 50%, 30%)`;
|
|
73
|
+
this.updateSvgColor(img, color);
|
|
74
|
+
}
|
|
75
|
+
/*
|
|
76
|
+
* Mimics OP Core's string hash function to ensure consistent color generation
|
|
77
|
+
* @see https://github.com/opf/openproject/blob/1b6eb3f9e45c3bdb05ce49d2cbe92995b87b4df5/frontend/src/app/shared/components/colors/colors.service.ts#L19-L26
|
|
78
|
+
*/
|
|
79
|
+
valueHash(value) {
|
|
80
|
+
let hash = 0;
|
|
81
|
+
for (let i = 0; i < value.length; i++) {
|
|
82
|
+
hash = value.charCodeAt(i) + ((hash << 5) - hash);
|
|
83
|
+
}
|
|
84
|
+
return hash % 360;
|
|
85
|
+
}
|
|
86
|
+
updateSvgColor(img, color) {
|
|
87
|
+
const dataUri = img.src;
|
|
88
|
+
if (!dataUri.startsWith('data:image/svg+xml;base64,'))
|
|
89
|
+
return;
|
|
90
|
+
const base64 = dataUri.replace('data:image/svg+xml;base64,', '');
|
|
91
|
+
try {
|
|
92
|
+
const svg = atob(base64);
|
|
93
|
+
const updatedSvg = svg.replace(/fill="hsl\([^"]+\)"/, `fill="${color}"`);
|
|
94
|
+
img.src = `data:image/svg+xml;base64,${btoa(updatedSvg)}`;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// If the SVG data is malformed or not valid base64, skip updating the color
|
|
98
|
+
// to avoid breaking the component.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
AvatarFallbackElement = __decorate([
|
|
103
|
+
controller
|
|
104
|
+
], AvatarFallbackElement);
|
|
105
|
+
export { AvatarFallbackElement };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* stylelint-disable selector-max-type, selector-max-specificity */
|
|
2
|
+
/* Type selectors are required for the avatar-fallback custom element wrapper.
|
|
3
|
+
Specificity overrides are needed to properly style nested avatar stacking. */
|
|
4
|
+
/* OpenProject AvatarStack - styles for avatar-fallback wrapper elements */
|
|
5
|
+
/* Make avatar-fallback invisible to layout - inner img acts as direct child */
|
|
6
|
+
.AvatarStack-body avatar-fallback {
|
|
7
|
+
display: contents;
|
|
8
|
+
}
|
|
9
|
+
/*
|
|
10
|
+
* Z-index stacking for avatars inside avatar-fallback wrappers.
|
|
11
|
+
* The base CSS uses .avatar:first-child/:last-child but those selectors
|
|
12
|
+
* don't match when .avatar is inside avatar-fallback (not a direct child).
|
|
13
|
+
*/
|
|
14
|
+
.AvatarStack-body avatar-fallback:first-child .avatar {
|
|
15
|
+
z-index: 3;
|
|
16
|
+
}
|
|
17
|
+
.AvatarStack-body avatar-fallback:nth-child(2) .avatar {
|
|
18
|
+
z-index: 2;
|
|
19
|
+
}
|
|
20
|
+
/* Hide 4th+ wrapped avatars */
|
|
21
|
+
.AvatarStack-body avatar-fallback:nth-child(n + 4) {
|
|
22
|
+
display: none;
|
|
23
|
+
opacity: 0;
|
|
24
|
+
}
|
|
25
|
+
/* Show all on hover/focus */
|
|
26
|
+
:is(.AvatarStack-body:hover:not([data-disable-expand]),.AvatarStack-body:focus-within:not([data-disable-expand])) avatar-fallback:nth-child(n + 4) {
|
|
27
|
+
display: contents;
|
|
28
|
+
opacity: 1;
|
|
29
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "open_project/avatar_stack",
|
|
3
|
+
"selectors": [
|
|
4
|
+
".AvatarStack-body avatar-fallback",
|
|
5
|
+
".AvatarStack-body avatar-fallback:first-child .avatar",
|
|
6
|
+
".AvatarStack-body avatar-fallback:nth-child(2) .avatar",
|
|
7
|
+
".AvatarStack-body avatar-fallback:nth-child(n + 4)",
|
|
8
|
+
":is(.AvatarStack-body:hover:not([data-disable-expand]),.AvatarStack-body:focus-within:not([data-disable-expand])) avatar-fallback:nth-child(n + 4)"
|
|
9
|
+
]
|
|
10
|
+
}
|
|
@@ -1,21 +1,83 @@
|
|
|
1
1
|
/* CSS for BorderBox::CollapsibleHeader */
|
|
2
2
|
|
|
3
3
|
.CollapsibleHeader {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
display: block;
|
|
5
|
+
container-name: collapsible-header-container;
|
|
6
|
+
container-type: inline-size;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
.Box:has(.CollapsibleHeader--collapsed) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
border-bottom-left-radius: 0;
|
|
11
|
+
border-bottom-right-radius: 0;
|
|
12
|
+
border-bottom-width: var(--borderWidth-thicker);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
.Box:has(.CollapsibleHeader--collapsed) .Box-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
.Box:has(.CollapsibleHeader--collapsed) .Box-list,.Box:has(.CollapsibleHeader--collapsed) .Box-body,.Box:has(.CollapsibleHeader--collapsed) .Box-footer {
|
|
16
|
+
display: none;
|
|
17
|
+
}
|
|
18
18
|
|
|
19
|
-
.CollapsibleHeader
|
|
19
|
+
.CollapsibleHeader-triggerArea {
|
|
20
|
+
cursor: pointer;
|
|
21
|
+
-webkit-user-select: none;
|
|
22
|
+
user-select: none;
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-wrap: nowrap;
|
|
25
|
+
align-items: center;
|
|
26
|
+
column-gap: var(--stack-gap-normal);
|
|
27
|
+
row-gap: var(--base-size-4);
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.CollapsibleHeader > .CollapsibleHeader-triggerArea {
|
|
32
|
+
flex: 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@container collapsible-header-container (width < 544px) {
|
|
36
|
+
.CollapsibleHeader-triggerArea {
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
align-items: flex-start;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.CollapsibleHeader[data-collapsed] .CollapsibleHeader-description {
|
|
42
|
+
display: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.CollapsibleHeader-description {
|
|
20
46
|
width: 100%;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.CollapsibleHeader--multi-line .CollapsibleHeader-triggerArea {
|
|
51
|
+
flex-direction: column;
|
|
52
|
+
align-items: flex-start;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.CollapsibleHeader--multi-line.CollapsibleHeader[data-collapsed] .CollapsibleHeader-description {
|
|
56
|
+
display: none;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.CollapsibleHeader--multi-line .CollapsibleHeader-description {
|
|
60
|
+
overflow: visible;
|
|
61
|
+
text-overflow: initial;
|
|
62
|
+
white-space: normal;
|
|
63
|
+
width: 100%;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.CollapsibleHeader-title-line {
|
|
67
|
+
flex-basis: 66%;
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
gap: var(--stack-gap-condensed);
|
|
71
|
+
overflow: hidden;
|
|
72
|
+
max-width: 100%;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.CollapsibleHeader-description {
|
|
76
|
+
flex: 1;
|
|
77
|
+
min-width: 0;
|
|
78
|
+
overflow: hidden;
|
|
79
|
+
text-overflow: ellipsis;
|
|
80
|
+
white-space: nowrap;
|
|
81
|
+
-webkit-user-select: text;
|
|
82
|
+
user-select: text;
|
|
21
83
|
}
|
|
@@ -3,9 +3,16 @@
|
|
|
3
3
|
"selectors": [
|
|
4
4
|
".CollapsibleHeader",
|
|
5
5
|
".Box:has(.CollapsibleHeader--collapsed)",
|
|
6
|
-
".Box:has(.CollapsibleHeader--collapsed) .Box-
|
|
6
|
+
".Box:has(.CollapsibleHeader--collapsed) .Box-list",
|
|
7
7
|
".Box:has(.CollapsibleHeader--collapsed) .Box-body",
|
|
8
8
|
".Box:has(.CollapsibleHeader--collapsed) .Box-footer",
|
|
9
|
-
".CollapsibleHeader
|
|
9
|
+
".CollapsibleHeader-triggerArea",
|
|
10
|
+
".CollapsibleHeader > .CollapsibleHeader-triggerArea",
|
|
11
|
+
".CollapsibleHeader[data-collapsed] .CollapsibleHeader-description",
|
|
12
|
+
".CollapsibleHeader-description",
|
|
13
|
+
".CollapsibleHeader--multi-line .CollapsibleHeader-triggerArea",
|
|
14
|
+
".CollapsibleHeader--multi-line.CollapsibleHeader[data-collapsed] .CollapsibleHeader-description",
|
|
15
|
+
".CollapsibleHeader--multi-line .CollapsibleHeader-description",
|
|
16
|
+
".CollapsibleHeader-title-line"
|
|
10
17
|
]
|
|
11
18
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
.InlineMessage {
|
|
2
|
+
display: grid;
|
|
3
|
+
/* stylelint-disable-next-line primer/typography */
|
|
4
|
+
font-size: var(--inline-message-fontSize);
|
|
5
|
+
/* stylelint-disable-next-line primer/typography */
|
|
6
|
+
line-height: var(--inline-message-lineHeight);
|
|
7
|
+
/* stylelint-disable-next-line primer/colors */
|
|
8
|
+
color: var(--inline-message-fgColor);
|
|
9
|
+
column-gap: 0.5rem;
|
|
10
|
+
grid-template-columns: auto 1fr;
|
|
11
|
+
align-items: start;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.InlineMessage[data-size='small'] {
|
|
15
|
+
--inline-message-fontSize: var(--text-body-size-small);
|
|
16
|
+
--inline-message-lineHeight: var(--text-body-lineHeight-small, 1.6666);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.InlineMessage[data-size='medium'] {
|
|
20
|
+
--inline-message-fontSize: var(--text-body-size-medium);
|
|
21
|
+
--inline-message-lineHeight: var(--text-body-lineHeight-medium, 1.4285);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.InlineMessage[data-variant='warning'] {
|
|
25
|
+
--inline-message-fgColor: var(--fgColor-attention);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.InlineMessage[data-variant='critical'] {
|
|
29
|
+
--inline-message-fgColor: var(--fgColor-danger);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.InlineMessage[data-variant='success'] {
|
|
33
|
+
--inline-message-fgColor: var(--fgColor-success);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.InlineMessage[data-variant='unavailable'] {
|
|
37
|
+
--inline-message-fgColor: var(--fgColor-muted);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.InlineMessageIcon {
|
|
41
|
+
min-height: calc(var(--inline-message-lineHeight) * var(--inline-message-fontSize));
|
|
42
|
+
}
|