openproject-primer_view_components 0.78.1 → 0.79.1

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/app/assets/javascripts/components/primer/open_project/avatar_fallback.d.ts +15 -0
  4. data/app/assets/javascripts/components/primer/primer.d.ts +1 -0
  5. data/app/assets/javascripts/primer_view_components.js +1 -1
  6. data/app/assets/javascripts/primer_view_components.js.map +1 -1
  7. data/app/assets/styles/primer_view_components.css +1 -1
  8. data/app/assets/styles/primer_view_components.css.map +1 -1
  9. data/app/components/primer/alpha/action_list.css +1 -1
  10. data/app/components/primer/alpha/action_list.css.map +1 -1
  11. data/app/components/primer/alpha/text_field.css +1 -1
  12. data/app/components/primer/alpha/text_field.css.map +1 -1
  13. data/app/components/primer/alpha/underline_nav.css +1 -1
  14. data/app/components/primer/alpha/underline_nav.css.map +1 -1
  15. data/app/components/primer/beta/avatar.rb +6 -2
  16. data/app/components/primer/beta/avatar_stack.css +1 -1
  17. data/app/components/primer/beta/avatar_stack.css.json +7 -4
  18. data/app/components/primer/beta/avatar_stack.css.map +1 -1
  19. data/app/components/primer/beta/avatar_stack.pcss +22 -2
  20. data/app/components/primer/beta/avatar_stack.rb +4 -1
  21. data/app/components/primer/beta/button.css +1 -1
  22. data/app/components/primer/beta/button.css.map +1 -1
  23. data/app/components/primer/open_project/avatar_fallback.d.ts +15 -0
  24. data/app/components/primer/open_project/avatar_fallback.js +102 -0
  25. data/app/components/primer/open_project/avatar_fallback.ts +94 -0
  26. data/app/components/primer/open_project/avatar_stack.css +1 -0
  27. data/app/components/primer/open_project/avatar_stack.css.json +10 -0
  28. data/app/components/primer/open_project/avatar_stack.css.map +1 -0
  29. data/app/components/primer/open_project/avatar_stack.pcss +40 -0
  30. data/app/components/primer/open_project/avatar_stack.rb +23 -0
  31. data/app/components/primer/open_project/avatar_with_fallback.rb +114 -0
  32. data/app/components/primer/primer.d.ts +1 -0
  33. data/app/components/primer/primer.js +1 -0
  34. data/app/components/primer/primer.pcss +1 -0
  35. data/app/components/primer/primer.ts +1 -0
  36. data/lib/primer/view_components/version.rb +1 -1
  37. data/previews/primer/beta/avatar_stack_preview.rb +23 -2
  38. data/previews/primer/open_project/avatar_stack_preview.rb +70 -0
  39. data/previews/primer/open_project/avatar_with_fallback_preview/fallback_multiple.html.erb +7 -0
  40. data/previews/primer/open_project/avatar_with_fallback_preview/fallback_sizes.html.erb +34 -0
  41. data/previews/primer/open_project/avatar_with_fallback_preview/multiple_broken_images.html.erb +10 -0
  42. data/previews/primer/open_project/avatar_with_fallback_preview.rb +91 -0
  43. data/static/arguments.json +110 -0
  44. data/static/audited_at.json +2 -0
  45. data/static/classes.json +2 -1
  46. data/static/constants.json +7 -0
  47. data/static/info_arch.json +399 -0
  48. data/static/previews.json +263 -0
  49. data/static/statuses.json +2 -0
  50. metadata +17 -2
@@ -1,3 +1,8 @@
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. */
5
+
1
6
  /* AvatarStack */
2
7
 
3
8
  /* Stacked avatars can be used to show who is participating in thread when
@@ -59,8 +64,14 @@
59
64
  opacity: 0;
60
65
  }
61
66
  }
67
+ /* stylelint-disable-next-line selector-max-type */
68
+ & span:nth-child(n + 4) .avatar {
69
+ display: none;
70
+ opacity: 0;
71
+ }
62
72
 
63
- &:hover {
73
+ &:hover:not([data-disable-expand]),
74
+ &:focus-within:not([data-disable-expand]) {
64
75
  & .avatar {
65
76
  margin-right: var(--base-size-4);
66
77
  }
@@ -69,11 +80,20 @@
69
80
  display: flex;
70
81
  opacity: 1;
71
82
  }
83
+ /* stylelint-disable-next-line selector-max-type */
84
+ & span:nth-child(n + 4) .avatar {
85
+ display: flex;
86
+ opacity: 1;
87
+ }
72
88
 
73
89
  & .avatar-more {
74
90
  display: none !important;
75
91
  }
76
92
  }
93
+
94
+ &[data-disable-expand] {
95
+ position: relative;
96
+ }
77
97
  }
78
98
 
79
99
  .avatar.avatar-more {
@@ -110,7 +130,7 @@
110
130
  right: 0;
111
131
  flex-direction: row-reverse;
112
132
 
113
- &:hover .avatar {
133
+ &:hover:not([data-disable-expand]) .avatar {
114
134
  margin-right: 0;
115
135
  margin-left: var(--base-size-4);
116
136
  }
@@ -22,14 +22,16 @@ module Primer
22
22
  # @param tag [Symbol] <%= one_of(Primer::Beta::AvatarStack::TAG_OPTIONS) %>
23
23
  # @param align [Symbol] <%= one_of(Primer::Beta::AvatarStack::ALIGN_OPTIONS) %>
24
24
  # @param tooltipped [Boolean] Whether to add a tooltip to the stack or not.
25
+ # @param disable_expand [Boolean] Whether to disable the expand behavior on hover. If true, avatars will not expand.
25
26
  # @param body_arguments [Hash] Parameters to add to the Body. If `tooltipped` is set, has the same arguments as <%= link_to_component(Primer::Tooltip) %>.
26
27
  # The default tag is <%= pretty_value(Primer::Beta::AvatarStack::DEFAULT_BODY_TAG) %> but can be changed using `tag:`
27
28
  # to <%= one_of(Primer::Beta::AvatarStack::BODY_TAG_OPTIONS, lower: true) %>
28
29
  # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
29
- def initialize(tag: DEFAULT_TAG, align: ALIGN_DEFAULT, tooltipped: false, body_arguments: {}, **system_arguments)
30
+ def initialize(tag: DEFAULT_TAG, align: ALIGN_DEFAULT, tooltipped: false, disable_expand: false, body_arguments: {}, **system_arguments)
30
31
  @align = fetch_or_fallback(ALIGN_OPTIONS, align, ALIGN_DEFAULT)
31
32
  @system_arguments = system_arguments
32
33
  @tooltipped = tooltipped
34
+ @disable_expand = disable_expand
33
35
  @body_arguments = body_arguments
34
36
  @direction = @body_arguments[:direction]
35
37
 
@@ -39,6 +41,7 @@ module Primer
39
41
  "AvatarStack-body",
40
42
  @body_arguments[:classes]
41
43
  )
44
+ @body_arguments[:"data-disable-expand"] = true if @disable_expand
42
45
 
43
46
  @system_arguments[:tag] = fetch_or_fallback(TAG_OPTIONS, tag, DEFAULT_TAG)
44
47
  @system_arguments[:classes] = class_names(
@@ -1 +1 @@
1
- :root{--duration-fast:80ms;--easing-easeInOut:cubic-bezier(0.65,0,0.35,1)}.Button{align-items:center;background-color:initial;border:var(--borderWidth-thin) solid;border-color:#0000;border-radius:var(--borderRadius-medium);color:var(--button-default-fgColor-rest);cursor:pointer;display:inline-flex;flex-direction:row;font-size:var(--text-body-size-medium);font-weight:var(--base-text-weight-medium);gap:var(--base-size-4);height:var(--control-medium-size);justify-content:space-between;min-width:max-content;padding:0 var(--control-medium-paddingInline-normal);position:relative;text-align:center;transition:var(--duration-fast) var(--easing-easeInOut);transition-property:color,fill,background-color,border-color;-webkit-user-select:none;user-select:none}@media (pointer:coarse){:is(.Button:before){content:"";height:100%;left:50%;min-height:48px;min-width:48px;position:absolute;top:50%;transform:translateX(-50%) translateY(-50%);width:100%}}.Button:hover{transition-duration:var(--duration-fast)}.Button:active{transition:none}.Button:disabled,.Button[aria-disabled=true]{box-shadow:none;cursor:not-allowed}.Button.Button--iconOnly{color:var(--fgColor-muted)}:is(a.Button,summary.Button):hover{-webkit-text-decoration:none;text-decoration:none}.Button-content{align-items:center;display:grid;flex:1 0 auto;grid-template-areas:"leadingVisual text trailingVisual";grid-template-columns:min-content minmax(0,auto) min-content;place-content:center}.Button-content>:not(:last-child){margin-right:var(--control-medium-gap)}.Button-content--alignStart{justify-content:start}.Button-visual{display:flex;pointer-events:none}.Button-visual .Counter{background-color:var(--buttonCounter-default-bgColor-rest);color:inherit}.Button-label{grid-area:text;line-height:var(--text-body-lineHeight-medium);white-space:nowrap}.Button-leadingVisual{grid-area:leadingVisual}.Button-leadingVisual svg{fill:currentcolor}.Button-trailingVisual{grid-area:trailingVisual}.Button-trailingAction{margin-right:calc(var(--base-size-4)*-1)}.Button--small{font-size:var(--text-body-size-small);gap:var(--control-small-gap);height:var(--control-small-size);min-width:var(--control-small-size);padding:0 var(--control-small-paddingInline-condensed)}.Button--small .Button-label{line-height:var(--text-body-lineHeight-small)}:is(.Button--small .Button-content)>:not(:last-child){margin-right:var(--control-small-gap)}.Button--large{gap:var(--control-large-gap);height:var(--control-large-size);padding:0 var(--control-large-paddingInline-spacious)}.Button--large .Button-label{line-height:var(--text-body-lineHeight-large)}:is(.Button--large .Button-content)>:not(:last-child){margin-right:var(--control-large-gap)}.Button--fullWidth{width:100%}.Button--labelWrap{height:unset;min-height:var(--control-medium-size);min-width:fit-content}.Button--labelWrap .Button-content{align-self:stretch;flex:1 1 auto;padding-block:calc(var(--control-medium-paddingBlock) - var(--base-size-2))}.Button--labelWrap .Button-label{white-space:unset}.Button--labelWrap.Button--small{height:unset;min-height:var(--control-small-size)}.Button--labelWrap.Button--small .Button-content{padding-block:calc(var(--control-small-paddingBlock) - var(--base-size-2))}.Button--labelWrap.Button--large{height:unset;min-height:var(--control-large-size);padding-inline:var(--control-large-paddingInline-spacious)}.Button--labelWrap.Button--large .Button-content{padding-block:calc(var(--control-large-paddingBlock) - var(--base-size-2))}.Button--primary{color:var(--button-primary-fgColor-rest);fill:var(--button-primary-iconColor-rest);background-color:var(--button-primary-bgColor-rest);border-color:var(--button-primary-borderColor-rest);box-shadow:var(--shadow-resting-small,var(--color-btn-primary-shadow))}.Button--primary.Button--iconOnly{color:var(--button-primary-iconColor-rest)}.Button--primary:hover:not(:disabled,.Button--inactive){background-color:var(--button-primary-bgColor-hover);border-color:var(--button-primary-borderColor-hover)}.Button--primary:focus{box-shadow:inset 0 0 0 3px var(--fgColor-onEmphasis);outline:2px solid var(--focus-outlineColor);outline-offset:-2px}.Button--primary:focus:not(:focus-visible){box-shadow:none;outline:1px solid #0000}.Button--primary:focus-visible{box-shadow:inset 0 0 0 3px var(--fgColor-onEmphasis);outline:2px solid var(--focus-outlineColor);outline-offset:-2px}.Button--primary:active:not(:disabled),.Button--primary[aria-pressed=true]{background-color:var(--button-primary-bgColor-active);box-shadow:var(--button-primary-shadow-selected)}.Button--primary:disabled,.Button--primary[aria-disabled=true]{color:var(--button-primary-fgColor-disabled);fill:var(--button-primary-fgColor-disabled);background-color:var(--button-primary-bgColor-disabled);border-color:var(--button-primary-borderColor-disabled)}.Button--primary .Counter{background-color:var(--buttonCounter-primary-bgColor-rest);color:inherit}.Button--secondary{color:var(--button-default-fgColor-rest);fill:var(--fgColor-muted);background-color:var(--button-default-bgColor-rest);border-color:var(--button-default-borderColor-rest);box-shadow:var(--button-default-shadow-resting),var(--button-default-shadow-inset)}.Button--secondary:hover:not(:disabled,.Button--inactive){background-color:var(--button-default-bgColor-hover);border-color:var(--button-default-borderColor-hover)}.Button--secondary:active:not(:disabled){background-color:var(--button-default-bgColor-active);border-color:var(--button-default-borderColor-active)}.Button--secondary[aria-pressed=true]{background-color:var(--button-default-bgColor-selected);box-shadow:var(--shadow-inset)}.Button--secondary:disabled,.Button--secondary[aria-disabled=true]{color:var(--control-fgColor-disabled);fill:var(--control-fgColor-disabled);background-color:var(--button-default-bgColor-disabled);border-color:var(--button-default-borderColor-disabled)}.Button--invisible{color:var(--button-invisible-fgColor-rest)}.Button--invisible.Button--iconOnly{color:var(--button-invisible-iconColor-rest,var(--color-fg-muted))}.Button--invisible:hover:not(:disabled,.Button--inactive){background-color:var(--control-transparent-bgColor-hover,var(--color-action-list-item-default-hover-bg))}.Button--invisible:active:not(:disabled),.Button--invisible[aria-pressed=true]{background-color:var(--button-invisible-bgColor-active)}.Button--invisible:disabled,.Button--invisible[aria-disabled=true]{color:var(--button-invisible-fgColor-disabled);fill:var(--button-invisible-fgColor-disabled);background-color:var(--button-invisible-bgColor-disabled);border-color:var(--button-invisible-borderColor-disabled)}.Button--invisible.Button--invisible-noVisuals .Button-label{color:var(--button-invisible-fgColor-rest)}.Button--invisible .Button-visual{color:var(--button-invisible-iconColor-rest,var(--color-fg-muted))}:is(.Button--invisible .Button-visual) .Counter{color:var(--fgColor-default)}.Button--link{color:var(--fgColor-link);display:inline-block;font-size:inherit;height:unset;min-width:fit-content;padding:0;fill:var(--fgColor-link);border:none}.Button--link:hover:not(:disabled,.Button--inactive){-webkit-text-decoration:underline;text-decoration:underline}.Button--link:focus,.Button--link:focus-visible{outline-offset:2px}.Button--link:disabled,.Button--link[aria-disabled=true]{color:var(--control-fgColor-disabled);fill:var(--control-fgColor-disabled);background-color:initial;border-color:#0000}.Button--link .Button-label{white-space:unset}.Button--danger{color:var(--button-danger-fgColor-rest);fill:var(--button-danger-iconColor-rest);background-color:var(--button-danger-bgColor-rest);border-color:var(--button-danger-borderColor-rest);box-shadow:var(--button-default-shadow-resting),var(--button-default-shadow-inset)}.Button--danger.Button--iconOnly{color:var(--button-danger-iconColor-rest)}.Button--danger:hover:not(:disabled,.Button--inactive){color:var(--button-danger-fgColor-hover);fill:var(--button-danger-fgColor-hover);background-color:var(--button-danger-bgColor-hover);border-color:var(--button-danger-borderColor-hover);box-shadow:var(--shadow-resting-small)}.Button--danger:hover:not(:disabled,.Button--inactive) .Counter{background-color:var(--buttonCounter-danger-bgColor-hover);color:var(--buttonCounter-danger-fgColor-hover)}.Button--danger:active:not(:disabled),.Button--danger[aria-pressed=true]{color:var(--button-danger-fgColor-active);fill:var(--button-danger-fgColor-active);background-color:var(--button-danger-bgColor-active);border-color:var(--button-danger-borderColor-active);box-shadow:var(--button-danger-shadow-selected)}.Button--danger:disabled,.Button--danger[aria-disabled=true]{color:var(--button-danger-fgColor-disabled);fill:var(--button-danger-fgColor-disabled);background-color:var(--button-danger-bgColor-disabled);border-color:var(--button-default-borderColor-disabled)}:is(.Button--danger:disabled,.Button--danger[aria-disabled=true]) .Counter{background-color:var(--buttonCounter-danger-bgColor-disabled);color:var(--buttonCounter-danger-fgColor-disabled)}.Button--danger .Counter{background-color:var(--buttonCounter-danger-bgColor-rest);color:var(--buttonCounter-danger-fgColor-rest)}.Button--iconOnly{display:inline-grid;padding:unset;place-content:center;width:var(--control-medium-size)}.Button--iconOnly.Button--small{width:var(--control-small-size)}.Button--iconOnly.Button--large{width:var(--control-large-size)}.Button--inactive:not([aria-disabled=true],:disabled){background-color:var(--button-inactive-bgColor);border:0;color:var(--button-inactive-fgColor);cursor:default}
1
+ :root{--duration-fast:80ms;--easing-easeInOut:cubic-bezier(0.65,0,0.35,1)}.Button{align-items:center;background-color:initial;border:var(--borderWidth-thin) solid;border-color:#0000;border-radius:var(--borderRadius-medium);color:var(--button-default-fgColor-rest);cursor:pointer;display:inline-flex;flex-direction:row;font-size:var(--text-body-size-medium);font-weight:var(--base-text-weight-medium);gap:var(--base-size-4);height:var(--control-medium-size);justify-content:space-between;min-width:max-content;padding:0 var(--control-medium-paddingInline-normal);position:relative;text-align:center;transition:var(--duration-fast) var(--easing-easeInOut);transition-property:color,fill,background-color,border-color;-webkit-user-select:none;user-select:none}@media (pointer:coarse){:is(.Button:before){content:"";height:100%;left:50%;min-height:48px;min-width:48px;position:absolute;top:50%;transform:translateX(-50%) translateY(-50%);width:100%}}.Button:hover{transition-duration:var(--duration-fast)}.Button:active{transition:none}.Button:disabled,.Button[aria-disabled=true]{box-shadow:none;cursor:not-allowed}.Button.Button--iconOnly{color:var(--fgColor-muted)}:is(a.Button,summary.Button):hover{-webkit-text-decoration:none;text-decoration:none}.Button-content{align-items:center;display:grid;flex:1 0 auto;grid-template-areas:"leadingVisual text trailingVisual";grid-template-columns:min-content minmax(0,auto) min-content;place-content:center}.Button-content>:not(:last-child){margin-right:var(--control-medium-gap)}.Button-content--alignStart{justify-content:start}.Button-visual{display:flex;pointer-events:none}.Button-visual .Counter{background-color:var(--buttonCounter-default-bgColor-rest);color:inherit}.Button-label{grid-area:text;line-height:var(--text-body-lineHeight-medium);white-space:nowrap}.Button-leadingVisual{grid-area:leadingVisual}.Button-leadingVisual svg{fill:currentcolor}.Button-trailingVisual{grid-area:trailingVisual}.Button-trailingAction{margin-right:calc(var(--base-size-4)*-1)}.Button--small{font-size:var(--text-body-size-small);gap:var(--control-small-gap);height:var(--control-small-size);min-width:var(--control-small-size);padding:0 var(--control-small-paddingInline-condensed)}.Button--small .Button-label{line-height:var(--text-body-lineHeight-small)}:is(.Button--small .Button-content)>:not(:last-child){margin-right:var(--control-small-gap)}.Button--large{gap:var(--control-large-gap);height:var(--control-large-size);padding:0 var(--control-large-paddingInline-spacious)}.Button--large .Button-label{line-height:var(--text-body-lineHeight-large)}:is(.Button--large .Button-content)>:not(:last-child){margin-right:var(--control-large-gap)}.Button--fullWidth{width:100%}.Button--labelWrap{height:unset;min-height:var(--control-medium-size);min-width:fit-content}.Button--labelWrap .Button-content{align-self:stretch;flex:1 1 auto;padding-block:calc(var(--control-medium-paddingBlock) - var(--base-size-2))}.Button--labelWrap .Button-label{white-space:unset}.Button--labelWrap.Button--small{height:unset;min-height:var(--control-small-size)}.Button--labelWrap.Button--small .Button-content{padding-block:calc(var(--control-small-paddingBlock) - var(--base-size-2))}.Button--labelWrap.Button--large{height:unset;min-height:var(--control-large-size);padding-inline:var(--control-large-paddingInline-spacious)}.Button--labelWrap.Button--large .Button-content{padding-block:calc(var(--control-large-paddingBlock) - var(--base-size-2))}.Button--primary{background-color:var(--button-primary-bgColor-rest);border-color:var(--button-primary-borderColor-rest);box-shadow:var(--shadow-resting-small,var(--color-btn-primary-shadow));color:var(--button-primary-fgColor-rest);fill:var(--button-primary-iconColor-rest)}.Button--primary.Button--iconOnly{color:var(--button-primary-iconColor-rest)}.Button--primary:hover:not(:disabled,.Button--inactive){background-color:var(--button-primary-bgColor-hover);border-color:var(--button-primary-borderColor-hover)}.Button--primary:focus{box-shadow:inset 0 0 0 3px var(--fgColor-onEmphasis);outline:2px solid var(--focus-outlineColor);outline-offset:-2px}.Button--primary:focus:not(:focus-visible){box-shadow:none;outline:1px solid #0000}.Button--primary:focus-visible{box-shadow:inset 0 0 0 3px var(--fgColor-onEmphasis);outline:2px solid var(--focus-outlineColor);outline-offset:-2px}.Button--primary:active:not(:disabled),.Button--primary[aria-pressed=true]{background-color:var(--button-primary-bgColor-active);box-shadow:var(--button-primary-shadow-selected)}.Button--primary:disabled,.Button--primary[aria-disabled=true]{background-color:var(--button-primary-bgColor-disabled);border-color:var(--button-primary-borderColor-disabled);color:var(--button-primary-fgColor-disabled);fill:var(--button-primary-fgColor-disabled)}.Button--primary .Counter{background-color:var(--buttonCounter-primary-bgColor-rest);color:inherit}.Button--secondary{background-color:var(--button-default-bgColor-rest);border-color:var(--button-default-borderColor-rest);box-shadow:var(--button-default-shadow-resting),var(--button-default-shadow-inset);color:var(--button-default-fgColor-rest);fill:var(--fgColor-muted)}.Button--secondary:hover:not(:disabled,.Button--inactive){background-color:var(--button-default-bgColor-hover);border-color:var(--button-default-borderColor-hover)}.Button--secondary:active:not(:disabled){background-color:var(--button-default-bgColor-active);border-color:var(--button-default-borderColor-active)}.Button--secondary[aria-pressed=true]{background-color:var(--button-default-bgColor-selected);box-shadow:var(--shadow-inset)}.Button--secondary:disabled,.Button--secondary[aria-disabled=true]{background-color:var(--button-default-bgColor-disabled);border-color:var(--button-default-borderColor-disabled);color:var(--control-fgColor-disabled);fill:var(--control-fgColor-disabled)}.Button--invisible{color:var(--button-invisible-fgColor-rest)}.Button--invisible.Button--iconOnly{color:var(--button-invisible-iconColor-rest,var(--color-fg-muted))}.Button--invisible:hover:not(:disabled,.Button--inactive){background-color:var(--control-transparent-bgColor-hover,var(--color-action-list-item-default-hover-bg))}.Button--invisible:active:not(:disabled),.Button--invisible[aria-pressed=true]{background-color:var(--button-invisible-bgColor-active)}.Button--invisible:disabled,.Button--invisible[aria-disabled=true]{background-color:var(--button-invisible-bgColor-disabled);border-color:var(--button-invisible-borderColor-disabled);color:var(--button-invisible-fgColor-disabled);fill:var(--button-invisible-fgColor-disabled)}.Button--invisible.Button--invisible-noVisuals .Button-label{color:var(--button-invisible-fgColor-rest)}.Button--invisible .Button-visual{color:var(--button-invisible-iconColor-rest,var(--color-fg-muted))}:is(.Button--invisible .Button-visual) .Counter{color:var(--fgColor-default)}.Button--link{border:none;color:var(--fgColor-link);display:inline-block;fill:var(--fgColor-link);font-size:inherit;height:unset;min-width:fit-content;padding:0}.Button--link:hover:not(:disabled,.Button--inactive){-webkit-text-decoration:underline;text-decoration:underline}.Button--link:focus,.Button--link:focus-visible{outline-offset:2px}.Button--link:disabled,.Button--link[aria-disabled=true]{background-color:initial;border-color:#0000;color:var(--control-fgColor-disabled);fill:var(--control-fgColor-disabled)}.Button--link .Button-label{white-space:unset}.Button--danger{background-color:var(--button-danger-bgColor-rest);border-color:var(--button-danger-borderColor-rest);box-shadow:var(--button-default-shadow-resting),var(--button-default-shadow-inset);color:var(--button-danger-fgColor-rest);fill:var(--button-danger-iconColor-rest)}.Button--danger.Button--iconOnly{color:var(--button-danger-iconColor-rest)}.Button--danger:hover:not(:disabled,.Button--inactive){background-color:var(--button-danger-bgColor-hover);border-color:var(--button-danger-borderColor-hover);box-shadow:var(--shadow-resting-small);color:var(--button-danger-fgColor-hover);fill:var(--button-danger-fgColor-hover)}.Button--danger:hover:not(:disabled,.Button--inactive) .Counter{background-color:var(--buttonCounter-danger-bgColor-hover);color:var(--buttonCounter-danger-fgColor-hover)}.Button--danger:active:not(:disabled),.Button--danger[aria-pressed=true]{background-color:var(--button-danger-bgColor-active);border-color:var(--button-danger-borderColor-active);box-shadow:var(--button-danger-shadow-selected);color:var(--button-danger-fgColor-active);fill:var(--button-danger-fgColor-active)}.Button--danger:disabled,.Button--danger[aria-disabled=true]{background-color:var(--button-danger-bgColor-disabled);border-color:var(--button-default-borderColor-disabled);color:var(--button-danger-fgColor-disabled);fill:var(--button-danger-fgColor-disabled)}:is(.Button--danger:disabled,.Button--danger[aria-disabled=true]) .Counter{background-color:var(--buttonCounter-danger-bgColor-disabled);color:var(--buttonCounter-danger-fgColor-disabled)}.Button--danger .Counter{background-color:var(--buttonCounter-danger-bgColor-rest);color:var(--buttonCounter-danger-fgColor-rest)}.Button--iconOnly{display:inline-grid;padding:unset;place-content:center;width:var(--control-medium-size)}.Button--iconOnly.Button--small{width:var(--control-small-size)}.Button--iconOnly.Button--large{width:var(--control-large-size)}.Button--inactive:not([aria-disabled=true],:disabled){background-color:var(--button-inactive-bgColor);border:0;color:var(--button-inactive-fgColor);cursor:default}
@@ -1 +1 @@
1
- {"version":3,"sources":["button.pcss","<no source>","../../../../lib/postcss_mixins/focusOutlineOnEmphasis.pcss"],"names":[],"mappings":"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","file":"button.css","sourcesContent":["/* stylelint-disable selector-no-qualifying-type */\n/* stylelint-disable selector-max-type */\n/* stylelint-disable primer/spacing */\n\n/* CSS for Button */\n\n/* temporary, pre primitives release */\n:root {\n --duration-fast: 80ms;\n --easing-easeInOut: cubic-bezier(0.65, 0, 0.35, 1);\n}\n\n/* base button */\n.Button {\n position: relative;\n display: inline-flex;\n min-width: max-content;\n height: var(--control-medium-size);\n padding: 0 var(--control-medium-paddingInline-normal);\n font-size: var(--text-body-size-medium);\n font-weight: var(--base-text-weight-medium);\n color: var(--button-default-fgColor-rest);\n text-align: center;\n cursor: pointer;\n flex-direction: row;\n user-select: none;\n background-color: transparent;\n border: var(--borderWidth-thin) solid;\n border-color: transparent;\n border-radius: var(--borderRadius-medium);\n transition: var(--duration-fast) var(--easing-easeInOut);\n transition-property: color, fill, background-color, border-color;\n justify-content: space-between;\n align-items: center;\n gap: var(--base-size-4);\n\n /* mobile friendly sizing */\n @media (pointer: coarse) {\n &::before {\n @mixin minTouchTarget 48px, 48px;\n }\n }\n\n /* base states */\n\n &:hover {\n transition-duration: var(--duration-fast);\n }\n\n &:active {\n transition: none;\n }\n\n &:disabled,\n &[aria-disabled='true'] {\n cursor: not-allowed;\n box-shadow: none;\n }\n\n &.Button--iconOnly {\n color: var(--fgColor-muted);\n }\n}\n\na.Button,\nsummary.Button {\n &:hover {\n text-decoration: none;\n }\n}\n\n/* wrap grid content to allow trailingAction to lock-right */\n.Button-content {\n flex: 1 0 auto;\n display: grid;\n grid-template-areas: 'leadingVisual text trailingVisual';\n grid-template-columns: min-content minmax(0, auto) min-content;\n align-items: center;\n place-content: center;\n\n /* padding-bottom: 1px; optical alignment for firefox */\n\n & > :not(:last-child) {\n margin-right: var(--control-medium-gap);\n }\n}\n\n/* center child elements for fullWidth */\n.Button-content--alignStart {\n justify-content: start;\n}\n\n/* button child elements */\n\n/* align svg */\n.Button-visual {\n display: flex;\n pointer-events: none; /* allow click handler to work, avoiding visuals */\n\n & .Counter {\n color: inherit;\n background-color: var(--buttonCounter-default-bgColor-rest);\n }\n}\n\n.Button-label {\n line-height: var(--text-body-lineHeight-medium);\n white-space: nowrap;\n grid-area: text;\n}\n\n.Button-leadingVisual {\n grid-area: leadingVisual;\n}\n\n.Button-leadingVisual svg {\n fill: currentcolor;\n}\n\n.Button-trailingVisual {\n grid-area: trailingVisual;\n}\n\n.Button-trailingAction {\n margin-right: calc(var(--base-size-4) * -1);\n}\n\n/* sizes */\n\n.Button--small {\n min-width: var(--control-small-size);\n height: var(--control-small-size);\n padding: 0 var(--control-small-paddingInline-condensed);\n font-size: var(--text-body-size-small);\n gap: var(--control-small-gap);\n\n & .Button-label {\n line-height: var(--text-body-lineHeight-small);\n }\n\n & .Button-content {\n & > :not(:last-child) {\n margin-right: var(--control-small-gap);\n }\n }\n}\n\n.Button--large {\n height: var(--control-large-size);\n padding: 0 var(--control-large-paddingInline-spacious);\n gap: var(--control-large-gap);\n\n & .Button-label {\n line-height: var(--text-body-lineHeight-large);\n }\n\n & .Button-content {\n & > :not(:last-child) {\n margin-right: var(--control-large-gap);\n }\n }\n}\n\n.Button--fullWidth {\n width: 100%;\n}\n\n/* allow button label text to wrap */\n\n.Button--labelWrap {\n min-width: fit-content;\n height: unset;\n min-height: var(--control-medium-size);\n\n & .Button-content {\n flex: 1 1 auto;\n align-self: stretch;\n padding-block: calc(var(--control-medium-paddingBlock) - var(--base-size-2));\n }\n\n & .Button-label {\n white-space: unset;\n }\n\n &.Button--small {\n height: unset;\n min-height: var(--control-small-size);\n\n & .Button-content {\n padding-block: calc(var(--control-small-paddingBlock) - var(--base-size-2));\n }\n }\n\n &.Button--large {\n height: unset;\n min-height: var(--control-large-size);\n padding-inline: var(--control-large-paddingInline-spacious);\n\n & .Button-content {\n padding-block: calc(var(--control-large-paddingBlock) - var(--base-size-2));\n }\n }\n}\n\n/* variants */\n\n/* primary */\n.Button--primary {\n color: var(--button-primary-fgColor-rest);\n fill: var(--button-primary-iconColor-rest);\n background-color: var(--button-primary-bgColor-rest);\n border-color: var(--button-primary-borderColor-rest);\n box-shadow: var(--shadow-resting-small, var(--color-btn-primary-shadow));\n\n &.Button--iconOnly {\n color: var(--button-primary-iconColor-rest);\n }\n\n &:hover:not(:disabled, .Button--inactive) {\n background-color: var(--button-primary-bgColor-hover);\n border-color: var(--button-primary-borderColor-hover);\n }\n\n /* fallback :focus state */\n &:focus {\n @mixin focusOutlineOnEmphasis;\n\n /* remove fallback :focus if :focus-visible is supported */\n &:not(:focus-visible) {\n outline: solid 1px transparent;\n box-shadow: none;\n }\n }\n\n /* default focus state */\n &:focus-visible {\n @mixin focusOutlineOnEmphasis;\n }\n\n &:active:not(:disabled),\n &[aria-pressed='true'] {\n background-color: var(--button-primary-bgColor-active);\n box-shadow: var(--button-primary-shadow-selected);\n }\n\n &:disabled,\n &[aria-disabled='true'] {\n color: var(--button-primary-fgColor-disabled);\n fill: var(--button-primary-fgColor-disabled);\n background-color: var(--button-primary-bgColor-disabled);\n border-color: var(--button-primary-borderColor-disabled);\n }\n\n & .Counter {\n color: inherit;\n background-color: var(--buttonCounter-primary-bgColor-rest);\n }\n}\n\n/* default (secondary) */\n.Button--secondary {\n color: var(--button-default-fgColor-rest);\n fill: var(--fgColor-muted); /* help this */\n background-color: var(--button-default-bgColor-rest);\n border-color: var(--button-default-borderColor-rest);\n box-shadow: var(--button-default-shadow-resting), var(--button-default-shadow-inset);\n\n &:hover:not(:disabled, .Button--inactive) {\n background-color: var(--button-default-bgColor-hover);\n border-color: var(--button-default-borderColor-hover);\n }\n\n &:active:not(:disabled) {\n background-color: var(--button-default-bgColor-active);\n border-color: var(--button-default-borderColor-active);\n }\n\n &[aria-pressed='true'] {\n background-color: var(--button-default-bgColor-selected);\n box-shadow: var(--shadow-inset);\n }\n\n &:disabled,\n &[aria-disabled='true'] {\n color: var(--control-fgColor-disabled);\n fill: var(--control-fgColor-disabled);\n background-color: var(--button-default-bgColor-disabled);\n border-color: var(--button-default-borderColor-disabled);\n }\n}\n\n.Button--invisible {\n color: var(--button-invisible-fgColor-rest);\n\n &.Button--iconOnly {\n color: var(--button-invisible-iconColor-rest, var(--color-fg-muted));\n }\n\n &:hover:not(:disabled, .Button--inactive) {\n background-color: var(--control-transparent-bgColor-hover, var(--color-action-list-item-default-hover-bg));\n }\n\n &[aria-pressed='true'],\n &:active:not(:disabled) {\n background-color: var(--button-invisible-bgColor-active);\n }\n\n &:disabled,\n &[aria-disabled='true'] {\n color: var(--button-invisible-fgColor-disabled);\n fill: var(--button-invisible-fgColor-disabled);\n background-color: var(--button-invisible-bgColor-disabled);\n border-color: var(--button-invisible-borderColor-disabled);\n }\n\n /* if button has no visuals, use link blue for text */\n &.Button--invisible-noVisuals .Button-label {\n color: var(--button-invisible-fgColor-rest);\n }\n\n & .Button-visual {\n color: var(--button-invisible-iconColor-rest, var(--color-fg-muted));\n\n & .Counter {\n color: var(--fgColor-default);\n }\n }\n}\n\n.Button--link {\n display: inline-block;\n min-width: fit-content;\n height: unset;\n padding: 0;\n font-size: inherit;\n color: var(--fgColor-link);\n fill: var(--fgColor-link);\n border: none;\n\n &:hover:not(:disabled, .Button--inactive) {\n text-decoration: underline;\n }\n\n &:focus-visible,\n &:focus {\n outline-offset: 2px;\n }\n\n &:disabled,\n &[aria-disabled='true'] {\n color: var(--control-fgColor-disabled);\n fill: var(--control-fgColor-disabled);\n background-color: transparent;\n border-color: transparent;\n }\n\n & .Button-label {\n white-space: unset;\n }\n}\n\n/* danger */\n.Button--danger {\n color: var(--button-danger-fgColor-rest);\n fill: var(--button-danger-iconColor-rest);\n background-color: var(--button-danger-bgColor-rest);\n border-color: var(--button-danger-borderColor-rest);\n box-shadow: var(--button-default-shadow-resting), var(--button-default-shadow-inset);\n\n &.Button--iconOnly {\n color: var(--button-danger-iconColor-rest);\n }\n\n &:hover:not(:disabled, .Button--inactive) {\n color: var(--button-danger-fgColor-hover);\n fill: var(--button-danger-fgColor-hover);\n background-color: var(--button-danger-bgColor-hover);\n border-color: var(--button-danger-borderColor-hover);\n box-shadow: var(--shadow-resting-small);\n\n & .Counter {\n color: var(--buttonCounter-danger-fgColor-hover);\n background-color: var(--buttonCounter-danger-bgColor-hover);\n }\n }\n\n &:active:not(:disabled),\n &[aria-pressed='true'] {\n color: var(--button-danger-fgColor-active);\n fill: var(--button-danger-fgColor-active);\n background-color: var(--button-danger-bgColor-active);\n border-color: var(--button-danger-borderColor-active);\n box-shadow: var(--button-danger-shadow-selected);\n }\n\n &:disabled,\n &[aria-disabled='true'] {\n color: var(--button-danger-fgColor-disabled);\n fill: var(--button-danger-fgColor-disabled);\n background-color: var(--button-danger-bgColor-disabled);\n border-color: var(--button-default-borderColor-disabled);\n\n & .Counter {\n color: var(--buttonCounter-danger-fgColor-disabled);\n background-color: var(--buttonCounter-danger-bgColor-disabled);\n }\n }\n\n & .Counter {\n color: var(--buttonCounter-danger-fgColor-rest);\n background-color: var(--buttonCounter-danger-bgColor-rest);\n }\n}\n\n.Button--iconOnly {\n display: inline-grid;\n width: var(--control-medium-size);\n padding: unset;\n place-content: center;\n\n &.Button--small {\n width: var(--control-small-size);\n }\n\n &.Button--large {\n width: var(--control-large-size);\n }\n}\n\n/* `disabled` takes precedence over `inactive` */\n.Button--inactive:not([aria-disabled='true'], :disabled) {\n color: var(--button-inactive-fgColor);\n cursor: default;\n background-color: var(--button-inactive-bgColor);\n border: 0;\n}\n",null,"/* outline with fg box-shadow for buttons */\n@define-mixin focusOutlineOnEmphasis $outlineOffset: -2px, $outlineColor: var(--focus-outlineColor) {\n outline: 2px solid $outlineColor;\n outline-offset: $outlineOffset;\n box-shadow: inset 0 0 0 3px var(--fgColor-onEmphasis);\n}\n"]}
1
+ {"version":3,"sources":["button.pcss","<no source>","../../../../lib/postcss_mixins/focusOutlineOnEmphasis.pcss"],"names":[],"mappings":"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","file":"button.css","sourcesContent":["/* stylelint-disable selector-no-qualifying-type */\n/* stylelint-disable selector-max-type */\n/* stylelint-disable primer/spacing */\n\n/* CSS for Button */\n\n/* temporary, pre primitives release */\n:root {\n --duration-fast: 80ms;\n --easing-easeInOut: cubic-bezier(0.65, 0, 0.35, 1);\n}\n\n/* base button */\n.Button {\n position: relative;\n display: inline-flex;\n min-width: max-content;\n height: var(--control-medium-size);\n padding: 0 var(--control-medium-paddingInline-normal);\n font-size: var(--text-body-size-medium);\n font-weight: var(--base-text-weight-medium);\n color: var(--button-default-fgColor-rest);\n text-align: center;\n cursor: pointer;\n flex-direction: row;\n user-select: none;\n background-color: transparent;\n border: var(--borderWidth-thin) solid;\n border-color: transparent;\n border-radius: var(--borderRadius-medium);\n transition: var(--duration-fast) var(--easing-easeInOut);\n transition-property: color, fill, background-color, border-color;\n justify-content: space-between;\n align-items: center;\n gap: var(--base-size-4);\n\n /* mobile friendly sizing */\n @media (pointer: coarse) {\n &::before {\n @mixin minTouchTarget 48px, 48px;\n }\n }\n\n /* base states */\n\n &:hover {\n transition-duration: var(--duration-fast);\n }\n\n &:active {\n transition: none;\n }\n\n &:disabled,\n &[aria-disabled='true'] {\n cursor: not-allowed;\n box-shadow: none;\n }\n\n &.Button--iconOnly {\n color: var(--fgColor-muted);\n }\n}\n\na.Button,\nsummary.Button {\n &:hover {\n text-decoration: none;\n }\n}\n\n/* wrap grid content to allow trailingAction to lock-right */\n.Button-content {\n flex: 1 0 auto;\n display: grid;\n grid-template-areas: 'leadingVisual text trailingVisual';\n grid-template-columns: min-content minmax(0, auto) min-content;\n align-items: center;\n place-content: center;\n\n /* padding-bottom: 1px; optical alignment for firefox */\n\n & > :not(:last-child) {\n margin-right: var(--control-medium-gap);\n }\n}\n\n/* center child elements for fullWidth */\n.Button-content--alignStart {\n justify-content: start;\n}\n\n/* button child elements */\n\n/* align svg */\n.Button-visual {\n display: flex;\n pointer-events: none; /* allow click handler to work, avoiding visuals */\n\n & .Counter {\n color: inherit;\n background-color: var(--buttonCounter-default-bgColor-rest);\n }\n}\n\n.Button-label {\n line-height: var(--text-body-lineHeight-medium);\n white-space: nowrap;\n grid-area: text;\n}\n\n.Button-leadingVisual {\n grid-area: leadingVisual;\n}\n\n.Button-leadingVisual svg {\n fill: currentcolor;\n}\n\n.Button-trailingVisual {\n grid-area: trailingVisual;\n}\n\n.Button-trailingAction {\n margin-right: calc(var(--base-size-4) * -1);\n}\n\n/* sizes */\n\n.Button--small {\n min-width: var(--control-small-size);\n height: var(--control-small-size);\n padding: 0 var(--control-small-paddingInline-condensed);\n font-size: var(--text-body-size-small);\n gap: var(--control-small-gap);\n\n & .Button-label {\n line-height: var(--text-body-lineHeight-small);\n }\n\n & .Button-content {\n & > :not(:last-child) {\n margin-right: var(--control-small-gap);\n }\n }\n}\n\n.Button--large {\n height: var(--control-large-size);\n padding: 0 var(--control-large-paddingInline-spacious);\n gap: var(--control-large-gap);\n\n & .Button-label {\n line-height: var(--text-body-lineHeight-large);\n }\n\n & .Button-content {\n & > :not(:last-child) {\n margin-right: var(--control-large-gap);\n }\n }\n}\n\n.Button--fullWidth {\n width: 100%;\n}\n\n/* allow button label text to wrap */\n\n.Button--labelWrap {\n min-width: fit-content;\n height: unset;\n min-height: var(--control-medium-size);\n\n & .Button-content {\n flex: 1 1 auto;\n align-self: stretch;\n padding-block: calc(var(--control-medium-paddingBlock) - var(--base-size-2));\n }\n\n & .Button-label {\n white-space: unset;\n }\n\n &.Button--small {\n height: unset;\n min-height: var(--control-small-size);\n\n & .Button-content {\n padding-block: calc(var(--control-small-paddingBlock) - var(--base-size-2));\n }\n }\n\n &.Button--large {\n height: unset;\n min-height: var(--control-large-size);\n padding-inline: var(--control-large-paddingInline-spacious);\n\n & .Button-content {\n padding-block: calc(var(--control-large-paddingBlock) - var(--base-size-2));\n }\n }\n}\n\n/* variants */\n\n/* primary */\n.Button--primary {\n color: var(--button-primary-fgColor-rest);\n fill: var(--button-primary-iconColor-rest);\n background-color: var(--button-primary-bgColor-rest);\n border-color: var(--button-primary-borderColor-rest);\n box-shadow: var(--shadow-resting-small, var(--color-btn-primary-shadow));\n\n &.Button--iconOnly {\n color: var(--button-primary-iconColor-rest);\n }\n\n &:hover:not(:disabled, .Button--inactive) {\n background-color: var(--button-primary-bgColor-hover);\n border-color: var(--button-primary-borderColor-hover);\n }\n\n /* fallback :focus state */\n &:focus {\n @mixin focusOutlineOnEmphasis;\n\n /* remove fallback :focus if :focus-visible is supported */\n &:not(:focus-visible) {\n outline: solid 1px transparent;\n box-shadow: none;\n }\n }\n\n /* default focus state */\n &:focus-visible {\n @mixin focusOutlineOnEmphasis;\n }\n\n &:active:not(:disabled),\n &[aria-pressed='true'] {\n background-color: var(--button-primary-bgColor-active);\n box-shadow: var(--button-primary-shadow-selected);\n }\n\n &:disabled,\n &[aria-disabled='true'] {\n color: var(--button-primary-fgColor-disabled);\n fill: var(--button-primary-fgColor-disabled);\n background-color: var(--button-primary-bgColor-disabled);\n border-color: var(--button-primary-borderColor-disabled);\n }\n\n & .Counter {\n color: inherit;\n background-color: var(--buttonCounter-primary-bgColor-rest);\n }\n}\n\n/* default (secondary) */\n.Button--secondary {\n color: var(--button-default-fgColor-rest);\n fill: var(--fgColor-muted); /* help this */\n background-color: var(--button-default-bgColor-rest);\n border-color: var(--button-default-borderColor-rest);\n box-shadow: var(--button-default-shadow-resting), var(--button-default-shadow-inset);\n\n &:hover:not(:disabled, .Button--inactive) {\n background-color: var(--button-default-bgColor-hover);\n border-color: var(--button-default-borderColor-hover);\n }\n\n &:active:not(:disabled) {\n background-color: var(--button-default-bgColor-active);\n border-color: var(--button-default-borderColor-active);\n }\n\n &[aria-pressed='true'] {\n background-color: var(--button-default-bgColor-selected);\n box-shadow: var(--shadow-inset);\n }\n\n &:disabled,\n &[aria-disabled='true'] {\n color: var(--control-fgColor-disabled);\n fill: var(--control-fgColor-disabled);\n background-color: var(--button-default-bgColor-disabled);\n border-color: var(--button-default-borderColor-disabled);\n }\n}\n\n.Button--invisible {\n color: var(--button-invisible-fgColor-rest);\n\n &.Button--iconOnly {\n color: var(--button-invisible-iconColor-rest, var(--color-fg-muted));\n }\n\n &:hover:not(:disabled, .Button--inactive) {\n background-color: var(--control-transparent-bgColor-hover, var(--color-action-list-item-default-hover-bg));\n }\n\n &[aria-pressed='true'],\n &:active:not(:disabled) {\n background-color: var(--button-invisible-bgColor-active);\n }\n\n &:disabled,\n &[aria-disabled='true'] {\n color: var(--button-invisible-fgColor-disabled);\n fill: var(--button-invisible-fgColor-disabled);\n background-color: var(--button-invisible-bgColor-disabled);\n border-color: var(--button-invisible-borderColor-disabled);\n }\n\n /* if button has no visuals, use link blue for text */\n &.Button--invisible-noVisuals .Button-label {\n color: var(--button-invisible-fgColor-rest);\n }\n\n & .Button-visual {\n color: var(--button-invisible-iconColor-rest, var(--color-fg-muted));\n\n & .Counter {\n color: var(--fgColor-default);\n }\n }\n}\n\n.Button--link {\n display: inline-block;\n min-width: fit-content;\n height: unset;\n padding: 0;\n font-size: inherit;\n color: var(--fgColor-link);\n fill: var(--fgColor-link);\n border: none;\n\n &:hover:not(:disabled, .Button--inactive) {\n text-decoration: underline;\n }\n\n &:focus-visible,\n &:focus {\n outline-offset: 2px;\n }\n\n &:disabled,\n &[aria-disabled='true'] {\n color: var(--control-fgColor-disabled);\n fill: var(--control-fgColor-disabled);\n background-color: transparent;\n border-color: transparent;\n }\n\n & .Button-label {\n white-space: unset;\n }\n}\n\n/* danger */\n.Button--danger {\n color: var(--button-danger-fgColor-rest);\n fill: var(--button-danger-iconColor-rest);\n background-color: var(--button-danger-bgColor-rest);\n border-color: var(--button-danger-borderColor-rest);\n box-shadow: var(--button-default-shadow-resting), var(--button-default-shadow-inset);\n\n &.Button--iconOnly {\n color: var(--button-danger-iconColor-rest);\n }\n\n &:hover:not(:disabled, .Button--inactive) {\n color: var(--button-danger-fgColor-hover);\n fill: var(--button-danger-fgColor-hover);\n background-color: var(--button-danger-bgColor-hover);\n border-color: var(--button-danger-borderColor-hover);\n box-shadow: var(--shadow-resting-small);\n\n & .Counter {\n color: var(--buttonCounter-danger-fgColor-hover);\n background-color: var(--buttonCounter-danger-bgColor-hover);\n }\n }\n\n &:active:not(:disabled),\n &[aria-pressed='true'] {\n color: var(--button-danger-fgColor-active);\n fill: var(--button-danger-fgColor-active);\n background-color: var(--button-danger-bgColor-active);\n border-color: var(--button-danger-borderColor-active);\n box-shadow: var(--button-danger-shadow-selected);\n }\n\n &:disabled,\n &[aria-disabled='true'] {\n color: var(--button-danger-fgColor-disabled);\n fill: var(--button-danger-fgColor-disabled);\n background-color: var(--button-danger-bgColor-disabled);\n border-color: var(--button-default-borderColor-disabled);\n\n & .Counter {\n color: var(--buttonCounter-danger-fgColor-disabled);\n background-color: var(--buttonCounter-danger-bgColor-disabled);\n }\n }\n\n & .Counter {\n color: var(--buttonCounter-danger-fgColor-rest);\n background-color: var(--buttonCounter-danger-bgColor-rest);\n }\n}\n\n.Button--iconOnly {\n display: inline-grid;\n width: var(--control-medium-size);\n padding: unset;\n place-content: center;\n\n &.Button--small {\n width: var(--control-small-size);\n }\n\n &.Button--large {\n width: var(--control-large-size);\n }\n}\n\n/* `disabled` takes precedence over `inactive` */\n.Button--inactive:not([aria-disabled='true'], :disabled) {\n color: var(--button-inactive-fgColor);\n cursor: default;\n background-color: var(--button-inactive-bgColor);\n border: 0;\n}\n",null,"/* outline with fg box-shadow for buttons */\n@define-mixin focusOutlineOnEmphasis $outlineOffset: -2px, $outlineColor: var(--focus-outlineColor) {\n outline: 2px solid $outlineColor;\n outline-offset: $outlineOffset;\n box-shadow: inset 0 0 0 3px var(--fgColor-onEmphasis);\n}\n"]}
@@ -0,0 +1,15 @@
1
+ export declare class AvatarFallbackElement extends HTMLElement {
2
+ uniqueId: string;
3
+ altText: string;
4
+ fallbackSrc: string;
5
+ private img;
6
+ private boundErrorHandler?;
7
+ connectedCallback(): void;
8
+ disconnectedCallback(): void;
9
+ private isImageBroken;
10
+ private handleImageError;
11
+ private applyColor;
12
+ private valueHash;
13
+ private updateSvgColor;
14
+ private isFallbackImage;
15
+ }
@@ -0,0 +1,102 @@
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 { attr, controller } from '@github/catalyst';
8
+ let AvatarFallbackElement = class AvatarFallbackElement extends HTMLElement {
9
+ constructor() {
10
+ super(...arguments);
11
+ this.uniqueId = '';
12
+ this.altText = '';
13
+ this.fallbackSrc = '';
14
+ this.img = null;
15
+ }
16
+ connectedCallback() {
17
+ this.img = this.querySelector('img') ?? null;
18
+ if (!this.img)
19
+ return;
20
+ this.boundErrorHandler = () => this.handleImageError(this.img);
21
+ // Handle image load errors (404, network failure, etc.)
22
+ this.img.addEventListener('error', this.boundErrorHandler);
23
+ // Check if image already failed (error event fired before listener attached)
24
+ if (this.isImageBroken(this.img)) {
25
+ this.handleImageError(this.img);
26
+ }
27
+ else if (this.isFallbackImage(this.img)) {
28
+ this.applyColor(this.img);
29
+ }
30
+ }
31
+ disconnectedCallback() {
32
+ if (this.boundErrorHandler && this.img) {
33
+ this.img.removeEventListener('error', this.boundErrorHandler);
34
+ }
35
+ this.boundErrorHandler = undefined;
36
+ this.img = null;
37
+ }
38
+ isImageBroken(img) {
39
+ // Image is broken if loading completed but no actual image data loaded
40
+ // Skip check for data URIs (fallback SVGs) as they're always valid
41
+ return img.complete && img.naturalWidth === 0 && !img.src.startsWith('data:');
42
+ }
43
+ handleImageError(img) {
44
+ // Prevent infinite loop if fallback also fails
45
+ if (this.isFallbackImage(img))
46
+ return;
47
+ if (this.fallbackSrc) {
48
+ img.src = this.fallbackSrc;
49
+ this.applyColor(img);
50
+ }
51
+ }
52
+ applyColor(img) {
53
+ // If either uniqueId or altText is missing, skip color customization so the SVG
54
+ // keeps its default gray fill defined in the source and no color override is applied.
55
+ if (!this.uniqueId || !this.altText)
56
+ return;
57
+ const text = `${this.uniqueId}${this.altText}`;
58
+ const hue = this.valueHash(text);
59
+ const color = `hsl(${hue}, 50%, 30%)`;
60
+ this.updateSvgColor(img, color);
61
+ }
62
+ /*
63
+ * Mimics OP Core's string hash function to ensure consistent color generation
64
+ * @see https://github.com/opf/openproject/blob/1b6eb3f9e45c3bdb05ce49d2cbe92995b87b4df5/frontend/src/app/shared/components/colors/colors.service.ts#L19-L26
65
+ */
66
+ valueHash(value) {
67
+ let hash = 0;
68
+ for (let i = 0; i < value.length; i++) {
69
+ hash = value.charCodeAt(i) + ((hash << 5) - hash);
70
+ }
71
+ return hash % 360;
72
+ }
73
+ updateSvgColor(img, color) {
74
+ const dataUri = img.src;
75
+ const base64 = dataUri.replace('data:image/svg+xml;base64,', '');
76
+ try {
77
+ const svg = atob(base64);
78
+ const updatedSvg = svg.replace(/fill="hsl\([^"]+\)"/, `fill="${color}"`);
79
+ img.src = `data:image/svg+xml;base64,${btoa(updatedSvg)}`;
80
+ }
81
+ catch {
82
+ // If the SVG data is malformed or not valid base64, skip updating the color
83
+ // to avoid breaking the component.
84
+ }
85
+ }
86
+ isFallbackImage(img) {
87
+ return img.src === this.fallbackSrc;
88
+ }
89
+ };
90
+ __decorate([
91
+ attr
92
+ ], AvatarFallbackElement.prototype, "uniqueId", void 0);
93
+ __decorate([
94
+ attr
95
+ ], AvatarFallbackElement.prototype, "altText", void 0);
96
+ __decorate([
97
+ attr
98
+ ], AvatarFallbackElement.prototype, "fallbackSrc", void 0);
99
+ AvatarFallbackElement = __decorate([
100
+ controller
101
+ ], AvatarFallbackElement);
102
+ export { AvatarFallbackElement };
@@ -0,0 +1,94 @@
1
+ import {attr, controller} from '@github/catalyst'
2
+
3
+ @controller
4
+ export class AvatarFallbackElement extends HTMLElement {
5
+ @attr uniqueId = ''
6
+ @attr altText = ''
7
+ @attr fallbackSrc = ''
8
+
9
+ private img: HTMLImageElement | null = null
10
+ private boundErrorHandler?: () => void
11
+
12
+ connectedCallback() {
13
+ this.img = this.querySelector<HTMLImageElement>('img') ?? null
14
+ if (!this.img) return
15
+
16
+ this.boundErrorHandler = () => this.handleImageError(this.img!)
17
+
18
+ // Handle image load errors (404, network failure, etc.)
19
+ this.img.addEventListener('error', this.boundErrorHandler)
20
+
21
+ // Check if image already failed (error event fired before listener attached)
22
+ if (this.isImageBroken(this.img)) {
23
+ this.handleImageError(this.img)
24
+ } else if (this.isFallbackImage(this.img)) {
25
+ this.applyColor(this.img)
26
+ }
27
+ }
28
+
29
+ disconnectedCallback() {
30
+ if (this.boundErrorHandler && this.img) {
31
+ this.img.removeEventListener('error', this.boundErrorHandler)
32
+ }
33
+ this.boundErrorHandler = undefined
34
+ this.img = null
35
+ }
36
+
37
+ private isImageBroken(img: HTMLImageElement): boolean {
38
+ // Image is broken if loading completed but no actual image data loaded
39
+ // Skip check for data URIs (fallback SVGs) as they're always valid
40
+ return img.complete && img.naturalWidth === 0 && !img.src.startsWith('data:')
41
+ }
42
+
43
+ private handleImageError(img: HTMLImageElement) {
44
+ // Prevent infinite loop if fallback also fails
45
+ if (this.isFallbackImage(img)) return
46
+
47
+ if (this.fallbackSrc) {
48
+ img.src = this.fallbackSrc
49
+ this.applyColor(img)
50
+ }
51
+ }
52
+
53
+ private applyColor(img: HTMLImageElement) {
54
+ // If either uniqueId or altText is missing, skip color customization so the SVG
55
+ // keeps its default gray fill defined in the source and no color override is applied.
56
+ if (!this.uniqueId || !this.altText) return
57
+
58
+ const text = `${this.uniqueId}${this.altText}`
59
+ const hue = this.valueHash(text)
60
+ const color = `hsl(${hue}, 50%, 30%)`
61
+
62
+ this.updateSvgColor(img, color)
63
+ }
64
+
65
+ /*
66
+ * Mimics OP Core's string hash function to ensure consistent color generation
67
+ * @see https://github.com/opf/openproject/blob/1b6eb3f9e45c3bdb05ce49d2cbe92995b87b4df5/frontend/src/app/shared/components/colors/colors.service.ts#L19-L26
68
+ */
69
+ private valueHash(value: string): number {
70
+ let hash = 0
71
+ for (let i = 0; i < value.length; i++) {
72
+ hash = value.charCodeAt(i) + ((hash << 5) - hash)
73
+ }
74
+ return hash % 360
75
+ }
76
+
77
+ private updateSvgColor(img: HTMLImageElement, color: string) {
78
+ const dataUri = img.src
79
+ const base64 = dataUri.replace('data:image/svg+xml;base64,', '')
80
+
81
+ try {
82
+ const svg = atob(base64)
83
+ const updatedSvg = svg.replace(/fill="hsl\([^"]+\)"/, `fill="${color}"`)
84
+ img.src = `data:image/svg+xml;base64,${btoa(updatedSvg)}`
85
+ } catch {
86
+ // If the SVG data is malformed or not valid base64, skip updating the color
87
+ // to avoid breaking the component.
88
+ }
89
+ }
90
+
91
+ private isFallbackImage(img: HTMLImageElement): boolean {
92
+ return img.src === this.fallbackSrc
93
+ }
94
+ }
@@ -0,0 +1 @@
1
+ .AvatarStack-body avatar-fallback{display:contents}.AvatarStack-body avatar-fallback:first-child .avatar{z-index:3}.AvatarStack-body avatar-fallback:nth-child(2) .avatar{z-index:2}.AvatarStack-body avatar-fallback:nth-child(n+4){display:none;opacity:0}:is(.AvatarStack-body:hover:not([data-disable-expand]),.AvatarStack-body:focus-within:not([data-disable-expand])) avatar-fallback:nth-child(n+4){display:contents;opacity:1}
@@ -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
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["avatar_stack.pcss"],"names":[],"mappings":"AAQE,kCACE,gBACF,CAOA,sDACE,SACF,CAEA,uDACE,SACF,CAGA,iDACE,YAAa,CACb,SACF,CAKE,iJACE,gBAAiB,CACjB,SACF","file":"avatar_stack.css","sourcesContent":["/* stylelint-disable selector-max-type, selector-max-specificity */\n/* Type selectors are required for the avatar-fallback custom element wrapper.\n Specificity overrides are needed to properly style nested avatar stacking. */\n\n/* OpenProject AvatarStack - styles for avatar-fallback wrapper elements */\n\n.AvatarStack-body {\n /* Make avatar-fallback invisible to layout - inner img acts as direct child */\n & avatar-fallback {\n display: contents;\n }\n\n /*\n * Z-index stacking for avatars inside avatar-fallback wrappers.\n * The base CSS uses .avatar:first-child/:last-child but those selectors\n * don't match when .avatar is inside avatar-fallback (not a direct child).\n */\n & avatar-fallback:first-child .avatar {\n z-index: 3;\n }\n\n & avatar-fallback:nth-child(2) .avatar {\n z-index: 2;\n }\n\n /* Hide 4th+ wrapped avatars */\n & avatar-fallback:nth-child(n + 4) {\n display: none;\n opacity: 0;\n }\n\n /* Show all on hover/focus */\n &:hover:not([data-disable-expand]),\n &:focus-within:not([data-disable-expand]) {\n & avatar-fallback:nth-child(n + 4) {\n display: contents;\n opacity: 1;\n }\n }\n}\n"]}
@@ -0,0 +1,40 @@
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
+
5
+ /* OpenProject AvatarStack - styles for avatar-fallback wrapper elements */
6
+
7
+ .AvatarStack-body {
8
+ /* Make avatar-fallback invisible to layout - inner img acts as direct child */
9
+ & avatar-fallback {
10
+ display: contents;
11
+ }
12
+
13
+ /*
14
+ * Z-index stacking for avatars inside avatar-fallback wrappers.
15
+ * The base CSS uses .avatar:first-child/:last-child but those selectors
16
+ * don't match when .avatar is inside avatar-fallback (not a direct child).
17
+ */
18
+ & avatar-fallback:first-child .avatar {
19
+ z-index: 3;
20
+ }
21
+
22
+ & avatar-fallback:nth-child(2) .avatar {
23
+ z-index: 2;
24
+ }
25
+
26
+ /* Hide 4th+ wrapped avatars */
27
+ & avatar-fallback:nth-child(n + 4) {
28
+ display: none;
29
+ opacity: 0;
30
+ }
31
+
32
+ /* Show all on hover/focus */
33
+ &:hover:not([data-disable-expand]),
34
+ &:focus-within:not([data-disable-expand]) {
35
+ & avatar-fallback:nth-child(n + 4) {
36
+ display: contents;
37
+ opacity: 1;
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ module OpenProject
5
+ # OpenProject-specific AvatarStack that extends Primer::Beta::AvatarStack
6
+ # to support avatar fallbacks with initials.
7
+ #
8
+ # Uses a different slot name (avatar_with_fallbacks) to avoid conflicts with the parent's avatars slot.
9
+ class AvatarStack < Primer::Beta::AvatarStack
10
+ status :open_project
11
+
12
+ # Required list of stacked avatars with fallback support.
13
+ #
14
+ # @param kwargs [Hash] The same arguments as <%= link_to_component(Primer::OpenProject::AvatarWithFallback) %>.
15
+ renders_many :avatar_with_fallbacks, "Primer::OpenProject::AvatarWithFallback"
16
+
17
+ # Alias avatar_with_fallbacks as avatars for use in the template
18
+ def avatars
19
+ avatar_with_fallbacks
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Primer
4
+ module OpenProject
5
+ # OpenProject-specific Avatar component that extends Primer::Beta::Avatar
6
+ # to support fallback rendering with initials when no image source is provided.
7
+ #
8
+ # When `src` is nil, this component renders an SVG with initials extracted from
9
+ # the alt text. The AvatarFallbackElement web component then enhances it client-side
10
+ # by applying a consistent background color based on the user's unique_id (using the
11
+ # same hash function as OP Core for consistency).
12
+ #
13
+ # This component follows the "extension over mutation" pattern - it extends
14
+ # Primer::Beta::Avatar without modifying its interface, ensuring compatibility
15
+ # with upstream changes.
16
+ class AvatarWithFallback < Primer::Beta::Avatar
17
+ status :open_project
18
+
19
+ # @see
20
+ # - https://primer.style/foundations/typography/
21
+ # - https://github.com/primer/css/blob/main/src/support/variables/typography.scss
22
+ FONT_STACK = "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'"
23
+
24
+ # @param src [String] The source url of the avatar image. When nil or a broken URL, it renders a fallback with initials.
25
+ # @param alt [String] Alt text for the avatar. Used for accessibility and to generate initials when src is nil.
26
+ # @param size [Integer] <%= one_of(Primer::Beta::Avatar::SIZE_OPTIONS) %>
27
+ # @param shape [Symbol] Shape of the avatar. <%= one_of(Primer::Beta::Avatar::SHAPE_OPTIONS) %>
28
+ # @param href [String] The URL to link to. If used, component will be wrapped by an `<a>` tag.
29
+ # @param unique_id [String, Integer] Unique identifier for generating consistent avatar colors across renders.
30
+ # @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
31
+ def initialize(src: nil, alt: nil, size: DEFAULT_SIZE, shape: DEFAULT_SHAPE, href: nil, unique_id: nil, **system_arguments)
32
+ require_src_or_alt_arguments(src, alt)
33
+
34
+ @unique_id = unique_id
35
+ @fallback_svg = generate_fallback_svg(alt, size)
36
+ final_src = src.blank? ? @fallback_svg : src
37
+
38
+ super(src: final_src, alt: alt, size: size, shape: shape, href: href, **system_arguments)
39
+ end
40
+
41
+ def call
42
+ render(
43
+ Primer::BaseComponent.new(
44
+ tag: :"avatar-fallback",
45
+ data: {
46
+ unique_id: @unique_id,
47
+ alt_text: @system_arguments[:alt],
48
+ fallback_src: @fallback_svg
49
+ }
50
+ )
51
+ ) { super }
52
+ end
53
+
54
+ private
55
+
56
+ def require_src_or_alt_arguments(src, alt)
57
+ return if src.present? || alt.present?
58
+
59
+ raise ArgumentError, "`src` or `alt` is required"
60
+ end
61
+
62
+ def generate_fallback_svg(alt, size)
63
+ svg_content = content_tag(
64
+ :svg,
65
+ safe_join([
66
+ # Use a neutral dark gray as default to minimize flicker in both light/dark modes
67
+ # JS will replace with the hashed color (hsl(hue, 50%, 30%))
68
+ tag.rect(width: "100%", height: "100%", fill: "hsl(0, 0%, 35%)"),
69
+ content_tag(
70
+ :text,
71
+ extract_initials(alt),
72
+ x: "50%",
73
+ y: "50%",
74
+ "text-anchor": "middle",
75
+ "dominant-baseline": "central",
76
+ fill: "white",
77
+ "font-size": fallback_font_size(size),
78
+ "font-weight": "600",
79
+ "font-family": FONT_STACK,
80
+ style: "user-select: none; text-transform: uppercase;"
81
+ )
82
+ ]),
83
+ xmlns: "http://www.w3.org/2000/svg",
84
+ width: size,
85
+ height: size,
86
+ viewBox: "0 0 #{size} #{size}",
87
+ )
88
+
89
+ "data:image/svg+xml;base64,#{Base64.strict_encode64(svg_content)}"
90
+ end
91
+
92
+ def extract_initials(name)
93
+ name = name.to_s.strip
94
+ return "" if name.empty?
95
+
96
+ chars = name.chars
97
+ first = chars[0]&.upcase || ""
98
+
99
+ last_space = name.rindex(" ")
100
+ if last_space && last_space < name.length - 1
101
+ last = name[last_space + 1]&.upcase || ""
102
+ "#{first}#{last}"
103
+ else
104
+ first
105
+ end
106
+ end
107
+
108
+ def fallback_font_size(size)
109
+ # Font size is 45% of avatar size for good readability, with a minimum of 8px
110
+ [(size * 0.45).round, 8].max
111
+ end
112
+ end
113
+ end
114
+ end
@@ -30,6 +30,7 @@ import './alpha/tree_view/tree_view';
30
30
  import './alpha/tree_view/tree_view_icon_pair_element';
31
31
  import './alpha/tree_view/tree_view_sub_tree_node_element';
32
32
  import './alpha/tree_view/tree_view_include_fragment_element';
33
+ import './open_project/avatar_fallback';
33
34
  import './open_project/page_header_element';
34
35
  import './open_project/zen_mode_button';
35
36
  import './open_project/sub_header_element';
@@ -30,6 +30,7 @@ import './alpha/tree_view/tree_view';
30
30
  import './alpha/tree_view/tree_view_icon_pair_element';
31
31
  import './alpha/tree_view/tree_view_sub_tree_node_element';
32
32
  import './alpha/tree_view/tree_view_include_fragment_element';
33
+ import './open_project/avatar_fallback';
33
34
  import './open_project/page_header_element';
34
35
  import './open_project/zen_mode_button';
35
36
  import './open_project/sub_header_element';
@@ -45,6 +45,7 @@
45
45
  @import "./alpha/action_bar.pcss";
46
46
 
47
47
  /* OP specifics */
48
+ @import "./open_project/avatar_stack.pcss";
48
49
  @import "./open_project/page_header.pcss";
49
50
  @import "./open_project/drag_handle.pcss";
50
51
  @import "./open_project/border_grid.pcss";
@@ -30,6 +30,7 @@ import './alpha/tree_view/tree_view'
30
30
  import './alpha/tree_view/tree_view_icon_pair_element'
31
31
  import './alpha/tree_view/tree_view_sub_tree_node_element'
32
32
  import './alpha/tree_view/tree_view_include_fragment_element'
33
+ import './open_project/avatar_fallback'
33
34
  import './open_project/page_header_element'
34
35
  import './open_project/zen_mode_button'
35
36
  import './open_project/sub_header_element'