@mixtint/primer-view-components 0.75.2 → 0.84.3
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 +354 -29
- 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 +1 -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 +28 -20
- 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 -41
- package/app/components/primer/beta/avatar_stack.css.json +7 -5
- 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 +51 -0
- package/app/components/primer/open_project/page_header.css.json +10 -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 +24 -23
- package/static/arguments.json +235 -9
- package/static/audited_at.json +6 -0
- package/static/classes.json +30 -2
- package/static/constants.json +67 -1
- package/static/form_previews.json +15 -0
- package/static/info_arch.json +1035 -257
- package/static/previews.json +476 -14
- 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;
|
|
@@ -38,53 +35,52 @@
|
|
|
38
35
|
/* stylelint-disable-next-line primer/spacing */
|
|
39
36
|
margin-right: -11px;
|
|
40
37
|
background-color: var(--bgColor-default);
|
|
41
|
-
/* stylelint-disable-next-line primer/colors */
|
|
42
|
-
border-right: var(--borderWidth-thin) solid var(--bgColor-default);
|
|
43
38
|
border-radius: var(--borderRadius-small);
|
|
44
39
|
transition: margin 0.1s ease-in-out;
|
|
45
40
|
}
|
|
46
|
-
|
|
47
41
|
:is(.AvatarStack-body .avatar):first-child {
|
|
48
42
|
z-index: 3;
|
|
49
43
|
}
|
|
50
|
-
|
|
51
44
|
:is(.AvatarStack-body .avatar):last-child {
|
|
52
45
|
z-index: 1;
|
|
53
|
-
border-right: 0;
|
|
54
46
|
}
|
|
55
|
-
|
|
56
47
|
/* stylelint-disable-next-line selector-max-type */
|
|
57
|
-
|
|
58
48
|
:is(.AvatarStack-body .avatar) img {
|
|
59
49
|
border-radius: var(--borderRadius-small);
|
|
60
50
|
}
|
|
61
|
-
|
|
62
51
|
/* Account for 4+ avatars */
|
|
63
|
-
|
|
64
52
|
:is(.AvatarStack-body .avatar):nth-child(n + 4) {
|
|
65
53
|
display: none;
|
|
66
54
|
opacity: 0;
|
|
67
55
|
}
|
|
68
|
-
|
|
69
|
-
.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 {
|
|
70
62
|
margin-right: var(--base-size-4);
|
|
71
63
|
}
|
|
72
|
-
|
|
73
|
-
.AvatarStack-body:hover .avatar:nth-child(n + 4) {
|
|
64
|
+
:is(.AvatarStack-body:hover:not([data-disable-expand]),.AvatarStack-body:focus-within:not([data-disable-expand])) .avatar:nth-child(n + 4) {
|
|
74
65
|
display: flex;
|
|
75
66
|
opacity: 1;
|
|
76
67
|
}
|
|
77
|
-
|
|
78
|
-
.AvatarStack-body:hover .avatar
|
|
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 {
|
|
70
|
+
display: flex;
|
|
71
|
+
opacity: 1;
|
|
72
|
+
}
|
|
73
|
+
:is(.AvatarStack-body:hover:not([data-disable-expand]),.AvatarStack-body:focus-within:not([data-disable-expand])) .avatar-more {
|
|
79
74
|
display: none !important;
|
|
80
75
|
}
|
|
81
|
-
|
|
76
|
+
.AvatarStack-body[data-disable-expand] {
|
|
77
|
+
position: relative;
|
|
78
|
+
}
|
|
82
79
|
.avatar.avatar-more {
|
|
83
80
|
z-index: 1;
|
|
84
81
|
margin-right: 0;
|
|
85
82
|
background: var(--bgColor-muted);
|
|
86
83
|
}
|
|
87
|
-
|
|
88
84
|
.avatar.avatar-more::before,.avatar.avatar-more::after {
|
|
89
85
|
position: absolute;
|
|
90
86
|
display: block;
|
|
@@ -94,51 +90,35 @@
|
|
|
94
90
|
border-radius: 2px;
|
|
95
91
|
outline: var(--borderWidth-thin) solid var(--bgColor-default);
|
|
96
92
|
}
|
|
97
|
-
|
|
98
93
|
.avatar.avatar-more::before {
|
|
99
94
|
width: 17px;
|
|
100
95
|
background: var(--avatarStack-fade-bgColor-muted);
|
|
101
96
|
}
|
|
102
|
-
|
|
103
97
|
.avatar.avatar-more::after {
|
|
104
98
|
width: 14px;
|
|
105
99
|
background: var(--avatarStack-fade-bgColor-default);
|
|
106
100
|
}
|
|
107
|
-
|
|
108
101
|
/* Right aligned variation */
|
|
109
|
-
|
|
110
102
|
.AvatarStack--right .AvatarStack-body {
|
|
111
103
|
right: 0;
|
|
112
104
|
flex-direction: row-reverse;
|
|
113
105
|
}
|
|
114
|
-
|
|
115
|
-
:is(.AvatarStack--right .AvatarStack-body):hover .avatar {
|
|
106
|
+
:is(.AvatarStack--right .AvatarStack-body):hover:not([data-disable-expand]) .avatar {
|
|
116
107
|
margin-right: 0;
|
|
117
108
|
margin-left: var(--base-size-4);
|
|
118
109
|
}
|
|
119
|
-
|
|
120
|
-
:is(.AvatarStack--right .AvatarStack-body) .avatar:not(:last-child) {
|
|
121
|
-
border-left: 0;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
110
|
.AvatarStack--right .avatar.avatar-more {
|
|
125
111
|
background: var(--avatarStack-fade-bgColor-default);
|
|
126
112
|
}
|
|
127
|
-
|
|
128
113
|
:is(.AvatarStack--right .avatar.avatar-more)::before {
|
|
129
114
|
width: 5px;
|
|
130
115
|
}
|
|
131
|
-
|
|
132
116
|
:is(.AvatarStack--right .avatar.avatar-more)::after {
|
|
133
117
|
width: 2px;
|
|
134
118
|
background: var(--bgColor-muted);
|
|
135
119
|
}
|
|
136
|
-
|
|
137
120
|
.AvatarStack--right .avatar {
|
|
138
121
|
margin-right: 0;
|
|
139
122
|
/* stylelint-disable-next-line primer/spacing */
|
|
140
123
|
margin-left: -11px;
|
|
141
|
-
border-right: 0;
|
|
142
|
-
/* stylelint-disable-next-line primer/colors */
|
|
143
|
-
border-left: var(--borderWidth-thin) solid var(--bgColor-default);
|
|
144
124
|
}
|
|
@@ -11,15 +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",
|
|
22
|
-
":is(.AvatarStack--right .AvatarStack-body) .avatar:not(:last-child)",
|
|
24
|
+
":is(.AvatarStack--right .AvatarStack-body):hover:not([data-disable-expand]) .avatar",
|
|
23
25
|
".AvatarStack--right .avatar.avatar-more",
|
|
24
26
|
":is(.AvatarStack--right .avatar.avatar-more)::before",
|
|
25
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
|
}
|