openproject-primer_view_components 0.79.1 → 0.80.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/app/assets/javascripts/components/primer/open_project/avatar_fallback.d.ts +20 -7
- data/app/assets/javascripts/primer_view_components.js +1 -1
- data/app/assets/javascripts/primer_view_components.js.map +1 -1
- data/app/assets/styles/primer_view_components.css +1 -1
- data/app/assets/styles/primer_view_components.css.map +1 -1
- data/app/components/primer/open_project/avatar_fallback.d.ts +20 -7
- data/app/components/primer/open_project/avatar_fallback.js +47 -44
- data/app/components/primer/open_project/avatar_fallback.ts +49 -38
- data/app/components/primer/open_project/avatar_with_fallback.rb +11 -12
- data/app/components/primer/open_project/fieldset.html.erb +8 -0
- data/app/components/primer/open_project/fieldset.rb +66 -0
- data/app/components/primer/open_project/inline_message.css +1 -0
- data/app/components/primer/open_project/inline_message.css.json +13 -0
- data/app/components/primer/open_project/inline_message.css.map +1 -0
- data/app/components/primer/open_project/inline_message.html.erb +4 -0
- data/app/components/primer/open_project/inline_message.pcss +42 -0
- data/app/components/primer/open_project/inline_message.rb +65 -0
- data/app/components/primer/open_project/page_header.rb +18 -1
- data/app/components/primer/primer.pcss +1 -0
- data/app/lib/primer/forms/dsl/fieldset_group_input.rb +35 -0
- data/app/lib/primer/forms/dsl/form_object.rb +4 -0
- data/app/lib/primer/forms/fieldset_group.html.erb +10 -0
- data/app/lib/primer/forms/fieldset_group.rb +54 -0
- data/lib/primer/view_components/version.rb +1 -1
- data/previews/primer/forms_preview/fieldset_group_form.html.erb +40 -0
- data/previews/primer/forms_preview.rb +3 -0
- data/previews/primer/open_project/inline_message_preview/default.html.erb +5 -0
- data/previews/primer/open_project/inline_message_preview/playground.html.erb +5 -0
- data/previews/primer/open_project/inline_message_preview.rb +22 -0
- data/static/arguments.json +67 -2
- data/static/audited_at.json +3 -0
- data/static/classes.json +6 -0
- data/static/constants.json +22 -0
- data/static/form_previews.json +5 -0
- data/static/info_arch.json +145 -3
- data/static/previews.json +34 -0
- data/static/statuses.json +3 -0
- metadata +17 -2
|
@@ -1,15 +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
|
+
*/
|
|
1
14
|
export declare class AvatarFallbackElement extends HTMLElement {
|
|
2
|
-
uniqueId: string;
|
|
3
|
-
altText: string;
|
|
4
|
-
fallbackSrc: string;
|
|
5
15
|
private img;
|
|
6
|
-
private
|
|
16
|
+
private testImage;
|
|
7
17
|
connectedCallback(): void;
|
|
8
18
|
disconnectedCallback(): void;
|
|
9
|
-
|
|
10
|
-
|
|
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;
|
|
11
25
|
private applyColor;
|
|
12
26
|
private valueHash;
|
|
13
27
|
private updateSvgColor;
|
|
14
|
-
private isFallbackImage;
|
|
15
28
|
}
|
|
@@ -4,57 +4,70 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
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
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import {
|
|
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
|
+
*/
|
|
8
21
|
let AvatarFallbackElement = class AvatarFallbackElement extends HTMLElement {
|
|
9
22
|
constructor() {
|
|
10
23
|
super(...arguments);
|
|
11
|
-
this.uniqueId = '';
|
|
12
|
-
this.altText = '';
|
|
13
|
-
this.fallbackSrc = '';
|
|
14
24
|
this.img = null;
|
|
25
|
+
this.testImage = null;
|
|
15
26
|
}
|
|
16
27
|
connectedCallback() {
|
|
17
28
|
this.img = this.querySelector('img') ?? null;
|
|
18
29
|
if (!this.img)
|
|
19
30
|
return;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
this.
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
this.applyColor(this.img);
|
|
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);
|
|
29
39
|
}
|
|
30
40
|
}
|
|
31
41
|
disconnectedCallback() {
|
|
32
|
-
|
|
33
|
-
|
|
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;
|
|
34
46
|
}
|
|
35
|
-
this.boundErrorHandler = undefined;
|
|
36
47
|
this.img = null;
|
|
37
48
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
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;
|
|
51
64
|
}
|
|
52
|
-
applyColor(img) {
|
|
65
|
+
applyColor(img, uniqueId, altText) {
|
|
53
66
|
// If either uniqueId or altText is missing, skip color customization so the SVG
|
|
54
67
|
// keeps its default gray fill defined in the source and no color override is applied.
|
|
55
|
-
if (!
|
|
68
|
+
if (!uniqueId || !altText)
|
|
56
69
|
return;
|
|
57
|
-
const text = `${
|
|
70
|
+
const text = `${uniqueId}${altText}`;
|
|
58
71
|
const hue = this.valueHash(text);
|
|
59
72
|
const color = `hsl(${hue}, 50%, 30%)`;
|
|
60
73
|
this.updateSvgColor(img, color);
|
|
@@ -72,6 +85,8 @@ let AvatarFallbackElement = class AvatarFallbackElement extends HTMLElement {
|
|
|
72
85
|
}
|
|
73
86
|
updateSvgColor(img, color) {
|
|
74
87
|
const dataUri = img.src;
|
|
88
|
+
if (!dataUri.startsWith('data:image/svg+xml;base64,'))
|
|
89
|
+
return;
|
|
75
90
|
const base64 = dataUri.replace('data:image/svg+xml;base64,', '');
|
|
76
91
|
try {
|
|
77
92
|
const svg = atob(base64);
|
|
@@ -83,19 +98,7 @@ let AvatarFallbackElement = class AvatarFallbackElement extends HTMLElement {
|
|
|
83
98
|
// to avoid breaking the component.
|
|
84
99
|
}
|
|
85
100
|
}
|
|
86
|
-
isFallbackImage(img) {
|
|
87
|
-
return img.src === this.fallbackSrc;
|
|
88
|
-
}
|
|
89
101
|
};
|
|
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
102
|
AvatarFallbackElement = __decorate([
|
|
100
103
|
controller
|
|
101
104
|
], AvatarFallbackElement);
|
|
@@ -1,61 +1,74 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {controller} from '@github/catalyst'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AvatarFallbackElement implements "fallback first" loading pattern:
|
|
5
|
+
* 1. Fallback SVG is rendered immediately as <img> src
|
|
6
|
+
* 2. Real avatar URL is test-loaded in background using new Image()
|
|
7
|
+
* 3. On success, swaps to real image; on failure, fallback stays visible
|
|
8
|
+
*
|
|
9
|
+
* This approach prevents flicker by never showing a broken image state.
|
|
10
|
+
* Inspired by OpenProject's Angular PrincipalRendererService.
|
|
11
|
+
*
|
|
12
|
+
* Note: We read attributes directly via getAttribute() instead of using @attr
|
|
13
|
+
* due to a Catalyst bug where @attr accessors aren't properly initialized
|
|
14
|
+
* when elements have pre-existing attribute values.
|
|
15
|
+
*/
|
|
3
16
|
@controller
|
|
4
17
|
export class AvatarFallbackElement extends HTMLElement {
|
|
5
|
-
@attr uniqueId = ''
|
|
6
|
-
@attr altText = ''
|
|
7
|
-
@attr fallbackSrc = ''
|
|
8
|
-
|
|
9
18
|
private img: HTMLImageElement | null = null
|
|
10
|
-
private
|
|
19
|
+
private testImage: HTMLImageElement | null = null
|
|
11
20
|
|
|
12
21
|
connectedCallback() {
|
|
13
22
|
this.img = this.querySelector<HTMLImageElement>('img') ?? null
|
|
14
23
|
if (!this.img) return
|
|
15
24
|
|
|
16
|
-
|
|
25
|
+
const uniqueId = this.getAttribute('data-unique-id') || ''
|
|
26
|
+
const altText = this.getAttribute('data-alt-text') || ''
|
|
27
|
+
const avatarSrc = this.getAttribute('data-avatar-src') || ''
|
|
17
28
|
|
|
18
|
-
//
|
|
19
|
-
this.img
|
|
29
|
+
// Apply hashed color to fallback SVG immediately
|
|
30
|
+
this.applyColor(this.img, uniqueId, altText)
|
|
20
31
|
|
|
21
|
-
//
|
|
22
|
-
if (
|
|
23
|
-
this.
|
|
24
|
-
} else if (this.isFallbackImage(this.img)) {
|
|
25
|
-
this.applyColor(this.img)
|
|
32
|
+
// Test-load real avatar URL in background
|
|
33
|
+
if (avatarSrc) {
|
|
34
|
+
this.testLoadImage(avatarSrc)
|
|
26
35
|
}
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
disconnectedCallback() {
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
// Clean up test image and its event handler to prevent memory leaks
|
|
40
|
+
if (this.testImage) {
|
|
41
|
+
this.testImage.onload = null
|
|
42
|
+
this.testImage = null
|
|
32
43
|
}
|
|
33
|
-
this.boundErrorHandler = undefined
|
|
34
44
|
this.img = null
|
|
35
45
|
}
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Test-loads the real avatar URL in background.
|
|
49
|
+
* On success, swaps the visible img to the real URL.
|
|
50
|
+
* On failure, does nothing - fallback stays visible.
|
|
51
|
+
*/
|
|
52
|
+
private testLoadImage(url: string) {
|
|
53
|
+
this.testImage = new Image()
|
|
54
|
+
|
|
55
|
+
this.testImage.onload = () => {
|
|
56
|
+
// Success - swap to real image
|
|
57
|
+
if (this.img) {
|
|
58
|
+
this.img.src = url
|
|
59
|
+
}
|
|
50
60
|
}
|
|
61
|
+
|
|
62
|
+
// On error: do nothing, fallback stays visible (no flicker)
|
|
63
|
+
this.testImage.src = url
|
|
51
64
|
}
|
|
52
65
|
|
|
53
|
-
private applyColor(img: HTMLImageElement) {
|
|
66
|
+
private applyColor(img: HTMLImageElement, uniqueId: string, altText: string) {
|
|
54
67
|
// If either uniqueId or altText is missing, skip color customization so the SVG
|
|
55
68
|
// keeps its default gray fill defined in the source and no color override is applied.
|
|
56
|
-
if (!
|
|
69
|
+
if (!uniqueId || !altText) return
|
|
57
70
|
|
|
58
|
-
const text = `${
|
|
71
|
+
const text = `${uniqueId}${altText}`
|
|
59
72
|
const hue = this.valueHash(text)
|
|
60
73
|
const color = `hsl(${hue}, 50%, 30%)`
|
|
61
74
|
|
|
@@ -76,6 +89,8 @@ export class AvatarFallbackElement extends HTMLElement {
|
|
|
76
89
|
|
|
77
90
|
private updateSvgColor(img: HTMLImageElement, color: string) {
|
|
78
91
|
const dataUri = img.src
|
|
92
|
+
if (!dataUri.startsWith('data:image/svg+xml;base64,')) return
|
|
93
|
+
|
|
79
94
|
const base64 = dataUri.replace('data:image/svg+xml;base64,', '')
|
|
80
95
|
|
|
81
96
|
try {
|
|
@@ -87,8 +102,4 @@ export class AvatarFallbackElement extends HTMLElement {
|
|
|
87
102
|
// to avoid breaking the component.
|
|
88
103
|
}
|
|
89
104
|
}
|
|
90
|
-
|
|
91
|
-
private isFallbackImage(img: HTMLImageElement): boolean {
|
|
92
|
-
return img.src === this.fallbackSrc
|
|
93
|
-
}
|
|
94
105
|
}
|
|
@@ -5,14 +5,12 @@ module Primer
|
|
|
5
5
|
# OpenProject-specific Avatar component that extends Primer::Beta::Avatar
|
|
6
6
|
# to support fallback rendering with initials when no image source is provided.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
8
|
+
# Uses a "fallback first" pattern for flicker-free loading:
|
|
9
|
+
# 1. Always renders fallback SVG as initial <img> src (visible immediately)
|
|
10
|
+
# 2. Client-side JS test-loads the real URL in background
|
|
11
|
+
# 3. On success, swaps to real image; on failure, fallback stays visible
|
|
12
12
|
#
|
|
13
|
-
# This
|
|
14
|
-
# Primer::Beta::Avatar without modifying its interface, ensuring compatibility
|
|
15
|
-
# with upstream changes.
|
|
13
|
+
# This approach is inspired by OpenProject's Angular PrincipalRendererService.
|
|
16
14
|
class AvatarWithFallback < Primer::Beta::Avatar
|
|
17
15
|
status :open_project
|
|
18
16
|
|
|
@@ -21,8 +19,8 @@ module Primer
|
|
|
21
19
|
# - https://github.com/primer/css/blob/main/src/support/variables/typography.scss
|
|
22
20
|
FONT_STACK = "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'"
|
|
23
21
|
|
|
24
|
-
# @param src [String] The source
|
|
25
|
-
# @param alt [String] Alt text for the avatar. Used for accessibility and to generate initials
|
|
22
|
+
# @param src [String] The source URL of the avatar image. When provided, JavaScript will test-load it in the background and swap to it on success. When nil or blank, only the fallback SVG is displayed.
|
|
23
|
+
# @param alt [String] Alt text for the avatar. Used for accessibility and to generate initials for the fallback SVG.
|
|
26
24
|
# @param size [Integer] <%= one_of(Primer::Beta::Avatar::SIZE_OPTIONS) %>
|
|
27
25
|
# @param shape [Symbol] Shape of the avatar. <%= one_of(Primer::Beta::Avatar::SHAPE_OPTIONS) %>
|
|
28
26
|
# @param href [String] The URL to link to. If used, component will be wrapped by an `<a>` tag.
|
|
@@ -32,10 +30,11 @@ module Primer
|
|
|
32
30
|
require_src_or_alt_arguments(src, alt)
|
|
33
31
|
|
|
34
32
|
@unique_id = unique_id
|
|
33
|
+
@avatar_src = src.presence
|
|
35
34
|
@fallback_svg = generate_fallback_svg(alt, size)
|
|
36
|
-
final_src = src.blank? ? @fallback_svg : src
|
|
37
35
|
|
|
38
|
-
|
|
36
|
+
# Always render fallback first - JS will swap to real image on successful load
|
|
37
|
+
super(src: @fallback_svg, alt: alt, size: size, shape: shape, href: href, **system_arguments)
|
|
39
38
|
end
|
|
40
39
|
|
|
41
40
|
def call
|
|
@@ -45,7 +44,7 @@ module Primer
|
|
|
45
44
|
data: {
|
|
46
45
|
unique_id: @unique_id,
|
|
47
46
|
alt_text: @system_arguments[:alt],
|
|
48
|
-
|
|
47
|
+
avatar_src: @avatar_src
|
|
49
48
|
}
|
|
50
49
|
)
|
|
51
50
|
) { super }
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Primer
|
|
4
|
+
module OpenProject
|
|
5
|
+
# A low-level component for building fieldsets with unopinionated styling.
|
|
6
|
+
#
|
|
7
|
+
# This component is not designed to be used directly, but rather a primitive for
|
|
8
|
+
# authors of other components and form controls.
|
|
9
|
+
class Fieldset < Primer::Component
|
|
10
|
+
status :open_project
|
|
11
|
+
|
|
12
|
+
attr_reader :legend_text
|
|
13
|
+
|
|
14
|
+
renders_one :legend, ->(**system_arguments) {
|
|
15
|
+
LegendComponent.new(visually_hide_legend: @visually_hide_legend, **system_arguments)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# @param legend_text [String] A legend should be short and concise. The String will also be read by assistive technology.
|
|
19
|
+
# @param visually_hide_legend [Boolean] Controls if the legend is visible. If `true`, screen reader only text will be added.
|
|
20
|
+
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
|
21
|
+
def initialize(legend_text: nil, visually_hide_legend: false, **system_arguments) # rubocop:disable Lint/MissingSuper
|
|
22
|
+
@legend_text = legend_text
|
|
23
|
+
@visually_hide_legend = visually_hide_legend
|
|
24
|
+
@system_arguments = deny_tag_argument(**system_arguments)
|
|
25
|
+
@system_arguments[:tag] = :fieldset
|
|
26
|
+
|
|
27
|
+
deny_aria_key(
|
|
28
|
+
:label,
|
|
29
|
+
"instead of `aria-label`, include `legend_text` and set `visually_hide_legend` to `true` on the component initializer.",
|
|
30
|
+
**@system_arguments
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def render?
|
|
35
|
+
content? && (legend_text.present? || legend?)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class LegendComponent < Primer::Component
|
|
39
|
+
status :open_project
|
|
40
|
+
|
|
41
|
+
attr_reader :text
|
|
42
|
+
|
|
43
|
+
def initialize(text: nil, visually_hide_legend: false, **system_arguments) # rubocop:disable Lint/MissingSuper
|
|
44
|
+
@text = text
|
|
45
|
+
|
|
46
|
+
@system_arguments = deny_tag_argument(**system_arguments)
|
|
47
|
+
@system_arguments[:tag] = :legend
|
|
48
|
+
@system_arguments[:classes] = class_names(
|
|
49
|
+
@system_arguments[:classes],
|
|
50
|
+
{ "sr-only" => visually_hide_legend }
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def call
|
|
55
|
+
render(Primer::BaseComponent.new(**@system_arguments)) { legend_content }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def legend_content
|
|
61
|
+
@legend_content ||= content || text
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.InlineMessage{align-items:start;color:var(--inline-message-fgColor);column-gap:.5rem;display:grid;font-size:var(--inline-message-fontSize);grid-template-columns:auto 1fr;line-height:var(--inline-message-lineHeight)}.InlineMessage[data-size=small]{--inline-message-fontSize:var(--text-body-size-small);--inline-message-lineHeight:var(--text-body-lineHeight-small,1.6666)}.InlineMessage[data-size=medium]{--inline-message-fontSize:var(--text-body-size-medium);--inline-message-lineHeight:var(--text-body-lineHeight-medium,1.4285)}.InlineMessage[data-variant=warning]{--inline-message-fgColor:var(--fgColor-attention)}.InlineMessage[data-variant=critical]{--inline-message-fgColor:var(--fgColor-danger)}.InlineMessage[data-variant=success]{--inline-message-fgColor:var(--fgColor-success)}.InlineMessage[data-variant=unavailable]{--inline-message-fgColor:var(--fgColor-muted)}.InlineMessageIcon{min-height:calc(var(--inline-message-lineHeight)*var(--inline-message-fontSize))}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "open_project/inline_message",
|
|
3
|
+
"selectors": [
|
|
4
|
+
".InlineMessage",
|
|
5
|
+
".InlineMessage[data-size=small]",
|
|
6
|
+
".InlineMessage[data-size=medium]",
|
|
7
|
+
".InlineMessage[data-variant=warning]",
|
|
8
|
+
".InlineMessage[data-variant=critical]",
|
|
9
|
+
".InlineMessage[data-variant=success]",
|
|
10
|
+
".InlineMessage[data-variant=unavailable]",
|
|
11
|
+
".InlineMessageIcon"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["inline_message.pcss"],"names":[],"mappings":"AAAA,eAUE,iBAAkB,CAHlB,mCAAoC,CACpC,gBAAkB,CAPlB,YAAa,CAEb,wCAAyC,CAMzC,8BAA+B,CAJ/B,4CAgCF,CAzBE,gCACE,qDAAsD,CACtD,oEACF,CAEA,iCACE,sDAAuD,CACvD,qEACF,CAEA,qCACE,iDACF,CAEA,sCACE,8CACF,CAEA,qCACE,+CACF,CAEA,yCACE,6CACF,CAGF,mBACE,gFACF","file":"inline_message.css","sourcesContent":[".InlineMessage {\n display: grid;\n /* stylelint-disable-next-line primer/typography */\n font-size: var(--inline-message-fontSize);\n /* stylelint-disable-next-line primer/typography */\n line-height: var(--inline-message-lineHeight);\n /* stylelint-disable-next-line primer/colors */\n color: var(--inline-message-fgColor);\n column-gap: 0.5rem;\n grid-template-columns: auto 1fr;\n align-items: start;\n\n &[data-size='small'] {\n --inline-message-fontSize: var(--text-body-size-small);\n --inline-message-lineHeight: var(--text-body-lineHeight-small, 1.6666);\n }\n\n &[data-size='medium'] {\n --inline-message-fontSize: var(--text-body-size-medium);\n --inline-message-lineHeight: var(--text-body-lineHeight-medium, 1.4285);\n }\n\n &[data-variant='warning'] {\n --inline-message-fgColor: var(--fgColor-attention);\n }\n\n &[data-variant='critical'] {\n --inline-message-fgColor: var(--fgColor-danger);\n }\n\n &[data-variant='success'] {\n --inline-message-fgColor: var(--fgColor-success);\n }\n\n &[data-variant='unavailable'] {\n --inline-message-fgColor: var(--fgColor-muted);\n }\n}\n\n.InlineMessageIcon {\n min-height: calc(var(--inline-message-lineHeight) * var(--inline-message-fontSize));\n}"]}
|
|
@@ -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
|
+
&[data-size='small'] {
|
|
14
|
+
--inline-message-fontSize: var(--text-body-size-small);
|
|
15
|
+
--inline-message-lineHeight: var(--text-body-lineHeight-small, 1.6666);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
&[data-size='medium'] {
|
|
19
|
+
--inline-message-fontSize: var(--text-body-size-medium);
|
|
20
|
+
--inline-message-lineHeight: var(--text-body-lineHeight-medium, 1.4285);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&[data-variant='warning'] {
|
|
24
|
+
--inline-message-fgColor: var(--fgColor-attention);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
&[data-variant='critical'] {
|
|
28
|
+
--inline-message-fgColor: var(--fgColor-danger);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
&[data-variant='success'] {
|
|
32
|
+
--inline-message-fgColor: var(--fgColor-success);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
&[data-variant='unavailable'] {
|
|
36
|
+
--inline-message-fgColor: var(--fgColor-muted);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.InlineMessageIcon {
|
|
41
|
+
min-height: calc(var(--inline-message-lineHeight) * var(--inline-message-fontSize));
|
|
42
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Primer
|
|
4
|
+
module OpenProject
|
|
5
|
+
# A simple component to render warning text.
|
|
6
|
+
#
|
|
7
|
+
# The warning text is rendered in the "attention" Primer color and
|
|
8
|
+
# uses a leading alert Octicon for additional emphasis. This component
|
|
9
|
+
# is designed to be used "inline", e.g. table cells, and in places
|
|
10
|
+
# where a Banner component might be overkill.
|
|
11
|
+
class InlineMessage < Primer::Component
|
|
12
|
+
status :open_project
|
|
13
|
+
|
|
14
|
+
SCHEME_ICON_MAPPINGS = {
|
|
15
|
+
warning: :alert,
|
|
16
|
+
critical: :alert,
|
|
17
|
+
success: :"check-circle",
|
|
18
|
+
unavailable: :alert
|
|
19
|
+
}.freeze
|
|
20
|
+
private_constant :SCHEME_ICON_MAPPINGS
|
|
21
|
+
SCHEME_OPTIONS = SCHEME_ICON_MAPPINGS.keys.freeze
|
|
22
|
+
|
|
23
|
+
SCHEME_SMALL_ICON_MAPPINGS = {
|
|
24
|
+
warning: :"alert-fill",
|
|
25
|
+
critical: :"alert-fill",
|
|
26
|
+
success: :"check-circle-fill",
|
|
27
|
+
unavailable: :"alert-fill"
|
|
28
|
+
}.freeze
|
|
29
|
+
private_constant :SCHEME_SMALL_ICON_MAPPINGS
|
|
30
|
+
DEFAULT_SIZE = :medium
|
|
31
|
+
SIZE_OPTIONS = [:small, DEFAULT_SIZE].freeze
|
|
32
|
+
|
|
33
|
+
# @param scheme [Symbol] <%= one_of(Primer::OpenProject::InlineMessage::SCHEME_OPTIONS) %>
|
|
34
|
+
# @param size [Symbol] <%= one_of(Primer::OpenProject::InlineMessage::SIZE_OPTIONS) %>
|
|
35
|
+
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
|
|
36
|
+
def initialize(scheme:, size: DEFAULT_SIZE, **system_arguments) # rubocop:disable Lint/MissingSuper
|
|
37
|
+
resolved_scheme = fetch_or_fallback(SCHEME_OPTIONS, scheme)
|
|
38
|
+
resolved_size = fetch_or_fallback(SIZE_OPTIONS, size, DEFAULT_SIZE)
|
|
39
|
+
|
|
40
|
+
@system_arguments = system_arguments
|
|
41
|
+
@system_arguments[:tag] ||= :div
|
|
42
|
+
@system_arguments[:classes] = class_names(
|
|
43
|
+
@system_arguments[:classes],
|
|
44
|
+
"InlineMessage"
|
|
45
|
+
)
|
|
46
|
+
@system_arguments[:data] = merge_data(
|
|
47
|
+
@system_arguments,
|
|
48
|
+
{ data: { size: resolved_size, variant: resolved_scheme } }
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@icon_arguments = { classes: "InlineMessageIcon" }
|
|
52
|
+
if resolved_size == :small
|
|
53
|
+
@icon_arguments[:icon] = SCHEME_SMALL_ICON_MAPPINGS[resolved_scheme]
|
|
54
|
+
@icon_arguments[:size] = :xsmall
|
|
55
|
+
else
|
|
56
|
+
@icon_arguments[:icon] = SCHEME_ICON_MAPPINGS[resolved_scheme]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def render?
|
|
61
|
+
content.present?
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -25,6 +25,8 @@ module Primer
|
|
|
25
25
|
DEFAULT_PARENT_LINK_DISPLAY = [:block, :none].freeze
|
|
26
26
|
BREADCRUMB_TRUNCATE_AT = 200
|
|
27
27
|
|
|
28
|
+
DEFAULT_BUTTON_ACTION_SIZE = :medium
|
|
29
|
+
|
|
28
30
|
STATE_DEFAULT = :show
|
|
29
31
|
STATE_EDIT = :edit
|
|
30
32
|
STATE_OPTIONS = [STATE_DEFAULT, STATE_EDIT].freeze
|
|
@@ -68,6 +70,7 @@ module Primer
|
|
|
68
70
|
system_arguments[:icon] = icon
|
|
69
71
|
system_arguments[:"aria-label"] ||= label
|
|
70
72
|
system_arguments = set_action_arguments(system_arguments, scheme: scheme)
|
|
73
|
+
system_arguments = enforce_consistent_button_size!(system_arguments)
|
|
71
74
|
|
|
72
75
|
component = Primer::Beta::IconButton
|
|
73
76
|
create_mobile_alternatives(component, mobile_icon, label, scheme, **system_arguments, &block)
|
|
@@ -78,6 +81,7 @@ module Primer
|
|
|
78
81
|
deny_tag_argument(**system_arguments)
|
|
79
82
|
|
|
80
83
|
system_arguments = set_action_arguments(system_arguments, scheme: scheme)
|
|
84
|
+
system_arguments = enforce_consistent_button_size!(system_arguments)
|
|
81
85
|
|
|
82
86
|
component = Primer::Beta::Button
|
|
83
87
|
create_mobile_alternatives(component, mobile_icon, mobile_label, scheme, **system_arguments, &block)
|
|
@@ -88,6 +92,7 @@ module Primer
|
|
|
88
92
|
deny_tag_argument(**system_arguments)
|
|
89
93
|
|
|
90
94
|
system_arguments = set_action_arguments(system_arguments, scheme: DEFAULT_ACTION_SCHEME)
|
|
95
|
+
system_arguments = enforce_consistent_button_size!(system_arguments)
|
|
91
96
|
|
|
92
97
|
component = Primer::OpenProject::ZenModeButton
|
|
93
98
|
create_mobile_alternatives(component, mobile_icon, mobile_label, DEFAULT_ACTION_SCHEME, **system_arguments, &block)
|
|
@@ -120,6 +125,7 @@ module Primer
|
|
|
120
125
|
|
|
121
126
|
system_arguments[:button_arguments] ||= {}
|
|
122
127
|
system_arguments[:button_arguments] = set_action_arguments(system_arguments[:button_arguments])
|
|
128
|
+
system_arguments[:button_arguments] = enforce_consistent_button_size!(system_arguments[:button_arguments])
|
|
123
129
|
|
|
124
130
|
# Add the options individually to the mobile menu in the template
|
|
125
131
|
@desktop_menu_block = block
|
|
@@ -138,6 +144,7 @@ module Primer
|
|
|
138
144
|
system_arguments[:button_arguments] ||= {}
|
|
139
145
|
system_arguments[:button_arguments][:id] = "dialog-show-#{system_arguments[:dialog_arguments][:id]}"
|
|
140
146
|
system_arguments[:button_arguments] = set_action_arguments(system_arguments[:button_arguments])
|
|
147
|
+
system_arguments[:button_arguments] = enforce_consistent_button_size!(system_arguments[:button_arguments])
|
|
141
148
|
|
|
142
149
|
component = Primer::OpenProject::PageHeader::Dialog
|
|
143
150
|
create_mobile_alternatives(component, mobile_icon, mobile_label, :default, **system_arguments, &block)
|
|
@@ -293,7 +300,6 @@ module Primer
|
|
|
293
300
|
def set_action_arguments(system_arguments, scheme: nil)
|
|
294
301
|
system_arguments[:ml] ||= 2
|
|
295
302
|
system_arguments[:display] = %i[none flex]
|
|
296
|
-
system_arguments[:size] = :medium
|
|
297
303
|
system_arguments[:scheme] = scheme unless scheme.nil?
|
|
298
304
|
system_arguments[:classes] = class_names(
|
|
299
305
|
system_arguments[:classes],
|
|
@@ -304,6 +310,17 @@ module Primer
|
|
|
304
310
|
system_arguments
|
|
305
311
|
end
|
|
306
312
|
|
|
313
|
+
def enforce_consistent_button_size!(system_arguments)
|
|
314
|
+
size = system_arguments.fetch(:size, DEFAULT_BUTTON_ACTION_SIZE)
|
|
315
|
+
@page_header_button_action_size ||= size
|
|
316
|
+
unless size == @page_header_button_action_size
|
|
317
|
+
raise ArgumentError,
|
|
318
|
+
"PageHeader button actions must all use the same size. " \
|
|
319
|
+
"Set the same `size:` for every button-like action (or omit it to use #{DEFAULT_BUTTON_ACTION_SIZE.inspect} everywhere)."
|
|
320
|
+
end
|
|
321
|
+
system_arguments.merge(size: @page_header_button_action_size)
|
|
322
|
+
end
|
|
323
|
+
|
|
307
324
|
def create_mobile_alternatives(component, mobile_icon, mobile_label, scheme, **system_arguments, &block)
|
|
308
325
|
# All actions should collapse into a single actionMenu on mobile
|
|
309
326
|
add_option_to_mobile_menu(system_arguments, mobile_icon, mobile_label, scheme)
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
@import "./open_project/drag_handle.pcss";
|
|
51
51
|
@import "./open_project/border_grid.pcss";
|
|
52
52
|
@import "./open_project/input_group.pcss";
|
|
53
|
+
@import "./open_project/inline_message.pcss";
|
|
53
54
|
@import "./open_project/sub_header.pcss";
|
|
54
55
|
@import "./open_project/side_panel/section.pcss";
|
|
55
56
|
@import "./open_project/border_box/collapsible_header.pcss";
|