@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.
Files changed (71) hide show
  1. package/README.md +20 -1
  2. package/app/assets/javascripts/components/primer/alpha/tree_view/tree_view.d.ts +1 -0
  3. package/app/assets/javascripts/components/primer/open_project/avatar_fallback.d.ts +28 -0
  4. package/app/assets/javascripts/components/primer/primer.d.ts +2 -0
  5. package/app/assets/javascripts/lib/primer/forms/character_counter.d.ts +41 -0
  6. package/app/assets/javascripts/lib/primer/forms/primer_text_area.d.ts +13 -0
  7. package/app/assets/javascripts/lib/primer/forms/primer_text_field.d.ts +2 -0
  8. package/app/assets/javascripts/primer_view_components.js +1 -1
  9. package/app/assets/javascripts/primer_view_components.js.map +1 -1
  10. package/app/assets/styles/primer_view_components.css +343 -22
  11. package/app/assets/styles/primer_view_components.css.map +1 -1
  12. package/app/components/primer/alpha/action_bar_element.js +68 -77
  13. package/app/components/primer/alpha/action_list.css +1 -1
  14. package/app/components/primer/alpha/action_list.js +1 -1
  15. package/app/components/primer/alpha/action_menu/action_menu_element.js +5 -1
  16. package/app/components/primer/alpha/dialog.css +12 -0
  17. package/app/components/primer/alpha/dialog.css.json +2 -1
  18. package/app/components/primer/alpha/segmented_control.js +1 -1
  19. package/app/components/primer/alpha/select_panel_element.js +4 -2
  20. package/app/components/primer/alpha/tab_nav.css +8 -1
  21. package/app/components/primer/alpha/tab_nav.css.json +1 -0
  22. package/app/components/primer/alpha/toggle_switch.js +1 -1
  23. package/app/components/primer/alpha/tool_tip.js +13 -6
  24. package/app/components/primer/alpha/tree_view/tree_view.d.ts +1 -0
  25. package/app/components/primer/alpha/tree_view/tree_view.js +24 -17
  26. package/app/components/primer/alpha/tree_view/tree_view_icon_pair_element.js +1 -1
  27. package/app/components/primer/alpha/tree_view/tree_view_include_fragment_element.js +1 -1
  28. package/app/components/primer/alpha/tree_view/tree_view_sub_tree_node_element.js +25 -2
  29. package/app/components/primer/alpha/x_banner.js +1 -1
  30. package/app/components/primer/beta/avatar_stack.css +21 -31
  31. package/app/components/primer/beta/avatar_stack.css.json +7 -4
  32. package/app/components/primer/beta/blankslate.css +7 -0
  33. package/app/components/primer/beta/blankslate.css.json +1 -0
  34. package/app/components/primer/beta/details_toggle_element.js +1 -1
  35. package/app/components/primer/beta/nav_list.js +1 -1
  36. package/app/components/primer/beta/nav_list_group_element.js +1 -1
  37. package/app/components/primer/beta/state.css +1 -2
  38. package/app/components/primer/beta/timeline_item.css +2 -1
  39. package/app/components/primer/dialog_helper.js +24 -9
  40. package/app/components/primer/open_project/avatar_fallback.d.ts +28 -0
  41. package/app/components/primer/open_project/avatar_fallback.js +105 -0
  42. package/app/components/primer/open_project/avatar_stack.css +29 -0
  43. package/app/components/primer/open_project/avatar_stack.css.json +10 -0
  44. package/app/components/primer/open_project/border_box/collapsible_header.css +72 -10
  45. package/app/components/primer/open_project/border_box/collapsible_header.css.json +9 -2
  46. package/app/components/primer/open_project/inline_message.css +42 -0
  47. package/app/components/primer/open_project/inline_message.css.json +13 -0
  48. package/app/components/primer/open_project/page_header.css +40 -3
  49. package/app/components/primer/open_project/page_header.css.json +8 -1
  50. package/app/components/primer/open_project/pagination.css +87 -0
  51. package/app/components/primer/open_project/pagination.css.json +24 -0
  52. package/app/components/primer/primer.d.ts +2 -0
  53. package/app/components/primer/primer.js +2 -0
  54. package/app/components/primer/scrollable_region.js +1 -1
  55. package/app/lib/primer/forms/character_counter.d.ts +41 -0
  56. package/app/lib/primer/forms/character_counter.js +114 -0
  57. package/app/lib/primer/forms/primer_multi_input.js +1 -1
  58. package/app/lib/primer/forms/primer_text_area.d.ts +13 -0
  59. package/app/lib/primer/forms/primer_text_area.js +53 -0
  60. package/app/lib/primer/forms/primer_text_field.d.ts +2 -0
  61. package/app/lib/primer/forms/primer_text_field.js +17 -3
  62. package/app/lib/primer/forms/toggle_switch_input.js +1 -1
  63. package/package.json +23 -22
  64. package/static/arguments.json +234 -2
  65. package/static/audited_at.json +6 -0
  66. package/static/classes.json +27 -2
  67. package/static/constants.json +66 -1
  68. package/static/form_previews.json +15 -0
  69. package/static/info_arch.json +821 -82
  70. package/static/previews.json +432 -9
  71. 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:hover .avatar {
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
- .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) {
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:hover .avatar",
15
- ".AvatarStack-body:hover .avatar:nth-child(n + 4)",
16
- ".AvatarStack-body:hover .avatar-more",
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 {
@@ -9,6 +9,7 @@
9
9
  ".blankslate-icon",
10
10
  ".blankslate-image",
11
11
  ".blankslate-heading",
12
+ ".blankslate-description",
12
13
  ".blankslate-action",
13
14
  ".blankslate-action:first-of-type",
14
15
  ".blankslate-action:last-of-type",
@@ -60,6 +60,6 @@ __decorate([
60
60
  target
61
61
  ], DetailsToggleElement.prototype, "summaryTarget", void 0);
62
62
  DetailsToggleElement = __decorate([
63
- controller
63
+ controller('details-toggle')
64
64
  ], DetailsToggleElement);
65
65
  window.DetailsToggleElement = DetailsToggleElement;
@@ -187,6 +187,6 @@ __decorate([
187
187
  target
188
188
  ], NavListElement.prototype, "topLevelList", void 0);
189
189
  NavListElement = __decorate([
190
- controller
190
+ controller('nav-list')
191
191
  ], NavListElement);
192
192
  export { NavListElement };
@@ -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(--control-medium-lineBoxHeight);
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));
@@ -58,7 +58,8 @@
58
58
  .TimelineItem-body {
59
59
  min-width: 0;
60
60
  max-width: 100%;
61
- margin-top: var(--base-size-4);
61
+ /* stylelint-disable-next-line primer/spacing */
62
+ margin-top: calc(var(--base-size-4) + 1px);
62
63
  color: var(--fgColor-muted);
63
64
  flex: auto;
64
65
  }
@@ -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
- // In some older browsers, such as Chrome 122, when a top layer element (such as a dialog)
29
- // opens from within a popover, the "hide all popovers" internal algorithm runs, hiding
30
- // any popover that is currently open, regardless of whether or not another top layer element,
31
- // such as a <dialog> is nested inside.
32
- // See https://github.com/whatwg/html/issues/9998.
33
- // This is fixed by https://github.com/whatwg/html/pull/10116, but while we still support browsers
34
- // that present this bug, we must undo the work they did to hide ancestral popovers of the dialog:
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]:not(:popover-open)');
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
- cursor: pointer;
5
- display: flex;
6
- align-items: center;
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
- border-bottom-left-radius: 0;
11
- border-bottom-right-radius: 0;
12
- border-bottom-width: var(--borderWidth-thicker);
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-row,.Box:has(.CollapsibleHeader--collapsed) .Box-body,.Box:has(.CollapsibleHeader--collapsed) .Box-footer {
16
- display: none;
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--triggerArea {
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-row",
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--triggerArea"
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
+ }