@rlse/widget 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ChangesModal.js +2 -68
- package/dist/RlseWidgetEmbed.d.ts +11 -0
- package/dist/RlseWidgetEmbed.js +98 -0
- package/dist/RlseWidgetMenu.d.ts +13 -0
- package/dist/RlseWidgetMenu.js +151 -0
- package/dist/angular/components/rlse-widget.component.d.ts +53 -0
- package/dist/angular/components/rlse-widget.component.js +376 -0
- package/dist/angular/index.d.ts +4 -0
- package/dist/angular/index.js +5 -0
- package/dist/angular/services/widget-data.service.d.ts +14 -0
- package/dist/angular/services/widget-data.service.js +93 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/storage.d.ts +1 -11
- package/dist/storage.js +2 -80
- package/dist/svelte/index.d.ts +5 -0
- package/dist/svelte/index.js +5 -0
- package/dist/types.d.ts +2 -54
- package/dist/types.js +1 -12
- package/dist/useWidgetData.js +17 -38
- package/dist/vue/components/index.d.ts +3 -0
- package/dist/vue/components/index.js +5 -0
- package/dist/vue/composables/index.d.ts +2 -0
- package/dist/vue/composables/index.js +2 -0
- package/dist/vue/composables/useStorage.d.ts +13 -0
- package/dist/vue/composables/useStorage.js +12 -0
- package/dist/vue/composables/useWidgetData.d.ts +16 -0
- package/dist/vue/composables/useWidgetData.js +42 -0
- package/dist/vue/index.d.ts +6 -0
- package/dist/vue/index.js +8 -0
- package/package.json +53 -4
package/dist/ChangesModal.js
CHANGED
|
@@ -1,62 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect } from 'react';
|
|
3
|
+
import { renderDescription, getReleaseNotesUrl } from '@rlse/widget-core';
|
|
3
4
|
import { markAllChangesAsSeen, getUnreadCount } from './storage';
|
|
4
|
-
/**
|
|
5
|
-
* Render a change description for display in the widget.
|
|
6
|
-
* - HTML content (new format, stored after the rich-content migration): passed
|
|
7
|
-
* through directly after stripping event-handler attributes and javascript: URLs.
|
|
8
|
-
* Content is sanitised at the write path before storage.
|
|
9
|
-
* - Markdown content (legacy records): converted via simpleMarkdownToHtml so
|
|
10
|
-
* older records continue to render correctly without a DB migration.
|
|
11
|
-
*/
|
|
12
|
-
function renderDescription(content) {
|
|
13
|
-
if (!content)
|
|
14
|
-
return '';
|
|
15
|
-
if (content.trimStart().startsWith('<')) {
|
|
16
|
-
return content
|
|
17
|
-
.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi, '')
|
|
18
|
-
.replace(/href\s*=\s*"javascript:[^"]*"/gi, '')
|
|
19
|
-
.replace(/href\s*=\s*'javascript:[^']*'/gi, '');
|
|
20
|
-
}
|
|
21
|
-
return simpleMarkdownToHtml(content);
|
|
22
|
-
}
|
|
23
|
-
function simpleMarkdownToHtml(markdown) {
|
|
24
|
-
// Helper to sanitize URLs - only allow safe protocols
|
|
25
|
-
const isSafeUrl = (url) => {
|
|
26
|
-
try {
|
|
27
|
-
const parsed = new URL(url);
|
|
28
|
-
const protocol = parsed.protocol.replace(':', '');
|
|
29
|
-
return ['http', 'https', 'mailto', 'tel'].includes(protocol);
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
// Relative URLs are OK
|
|
33
|
-
return (url.startsWith('/') ||
|
|
34
|
-
url.startsWith('./') ||
|
|
35
|
-
url.startsWith('../') ||
|
|
36
|
-
!url.includes(':'));
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
return markdown
|
|
40
|
-
.replace(/&/g, '&')
|
|
41
|
-
.replace(/</g, '<')
|
|
42
|
-
.replace(/>/g, '>')
|
|
43
|
-
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
44
|
-
.replace(/__(.+?)__/g, '<strong>$1</strong>')
|
|
45
|
-
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
46
|
-
.replace(/_(.+?)_/g, '<em>$1</em>')
|
|
47
|
-
.replace(/`(.+?)`/g, '<code>$1</code>')
|
|
48
|
-
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
|
|
49
|
-
if (isSafeUrl(url)) {
|
|
50
|
-
// Escape quotes in URL to prevent injection
|
|
51
|
-
const safeUrl = url.replace(/"/g, '%22').replace(/'/g, '%27');
|
|
52
|
-
return `<a href="${safeUrl}" target="_blank" rel="noopener">${text}</a>`;
|
|
53
|
-
}
|
|
54
|
-
return text; // Return plain text for unsafe URLs
|
|
55
|
-
})
|
|
56
|
-
.split('\n\n')
|
|
57
|
-
.map((para) => `<p>${para}</p>`)
|
|
58
|
-
.join('');
|
|
59
|
-
}
|
|
60
5
|
function ChangeCard({ change, showStatus, showDates, }) {
|
|
61
6
|
const date = new Date(change._creationTime).toLocaleDateString('en-US', {
|
|
62
7
|
year: 'numeric',
|
|
@@ -110,18 +55,7 @@ export function ChangesModal({ config, changes, isLoading, isOpen, onClose, onMa
|
|
|
110
55
|
}, [isOpen, onClose]);
|
|
111
56
|
if (!isOpen)
|
|
112
57
|
return null;
|
|
113
|
-
const releaseNotesUrl = (
|
|
114
|
-
try {
|
|
115
|
-
const parsed = new URL(config.baseUrl);
|
|
116
|
-
const base = `${parsed.protocol}//${config.orgSlug}.${parsed.host}`;
|
|
117
|
-
return config.appSlug ? `${base}/${config.appSlug}` : base;
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
return config.appSlug
|
|
121
|
-
? `${config.baseUrl}/${config.orgSlug}/${config.appSlug}`
|
|
122
|
-
: `${config.baseUrl}/${config.orgSlug}`;
|
|
123
|
-
}
|
|
124
|
-
})();
|
|
58
|
+
const releaseNotesUrl = getReleaseNotesUrl(config.baseUrl, config.orgSlug, config.appSlug);
|
|
125
59
|
const unreadCount = getUnreadCount(config.orgSlug, changes);
|
|
126
60
|
return (_jsx("div", { role: "dialog", "aria-modal": "true", "aria-labelledby": "rlse-widget-title", onClick: (e) => {
|
|
127
61
|
if (e.target === e.currentTarget) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { WidgetConfig } from './types';
|
|
3
|
+
export interface RlseWidgetEmbedProps extends WidgetConfig {
|
|
4
|
+
/** Container style - pass CSS properties for the container */
|
|
5
|
+
containerStyle?: React.CSSProperties;
|
|
6
|
+
/** Show the header with title */
|
|
7
|
+
showHeader?: boolean;
|
|
8
|
+
/** Show the footer with "mark as read" and "view all" */
|
|
9
|
+
showFooter?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function RlseWidgetEmbed(props: RlseWidgetEmbedProps): React.ReactElement | null;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import { renderDescription, formatChangeDate, getReleaseNotesUrl } from '@rlse/widget-core';
|
|
4
|
+
import { DEFAULT_CONFIG } from './types';
|
|
5
|
+
import { useWidgetData } from './useWidgetData';
|
|
6
|
+
import { markAllChangesAsSeen, getUnreadCount } from './storage';
|
|
7
|
+
export function RlseWidgetEmbed(props) {
|
|
8
|
+
const config = { ...DEFAULT_CONFIG, ...props };
|
|
9
|
+
const { containerStyle, showHeader = true, showFooter = true } = props;
|
|
10
|
+
const { changes, isLoading } = useWidgetData(config.orgSlug, config.appSlug, config.limit, config.baseUrl);
|
|
11
|
+
const handleMarkAsRead = useCallback(() => {
|
|
12
|
+
const changeIds = changes.map((c) => c._id);
|
|
13
|
+
markAllChangesAsSeen(config.orgSlug, changeIds);
|
|
14
|
+
// Trigger re-render
|
|
15
|
+
window.dispatchEvent(new Event('storage'));
|
|
16
|
+
}, [changes, config.orgSlug]);
|
|
17
|
+
const unreadCount = getUnreadCount(config.orgSlug, changes);
|
|
18
|
+
const releaseNotesUrl = getReleaseNotesUrl(config.baseUrl, config.orgSlug, config.appSlug);
|
|
19
|
+
const defaultContainerStyle = {
|
|
20
|
+
background: 'var(--background, white)',
|
|
21
|
+
borderRadius: 12,
|
|
22
|
+
border: '1px solid var(--border, #e2e8f0)',
|
|
23
|
+
display: 'flex',
|
|
24
|
+
flexDirection: 'column',
|
|
25
|
+
maxHeight: '100%',
|
|
26
|
+
overflow: 'hidden',
|
|
27
|
+
};
|
|
28
|
+
const headerStyle = {
|
|
29
|
+
display: 'flex',
|
|
30
|
+
alignItems: 'center',
|
|
31
|
+
justifyContent: 'space-between',
|
|
32
|
+
padding: '16px 20px',
|
|
33
|
+
borderBottom: '1px solid var(--border, #e2e8f0)',
|
|
34
|
+
};
|
|
35
|
+
const titleStyle = {
|
|
36
|
+
margin: 0,
|
|
37
|
+
fontSize: 16,
|
|
38
|
+
fontWeight: 600,
|
|
39
|
+
};
|
|
40
|
+
const contentStyle = {
|
|
41
|
+
overflowY: 'auto',
|
|
42
|
+
padding: '0 20px',
|
|
43
|
+
flex: 1,
|
|
44
|
+
};
|
|
45
|
+
const footerStyle = {
|
|
46
|
+
display: 'flex',
|
|
47
|
+
alignItems: 'center',
|
|
48
|
+
justifyContent: 'space-between',
|
|
49
|
+
padding: '12px 20px',
|
|
50
|
+
borderTop: '1px solid var(--border, #e2e8f0)',
|
|
51
|
+
gap: 12,
|
|
52
|
+
};
|
|
53
|
+
return (_jsxs("div", { style: { ...defaultContainerStyle, ...containerStyle }, children: [showHeader && (_jsxs("div", { style: headerStyle, children: [_jsx("h2", { style: titleStyle, children: config.modalTitle }), unreadCount > 0 && (_jsx("span", { style: {
|
|
54
|
+
fontSize: 12,
|
|
55
|
+
fontWeight: 500,
|
|
56
|
+
padding: '2px 8px',
|
|
57
|
+
borderRadius: 10,
|
|
58
|
+
background: config.primaryColor || '#3b82f6',
|
|
59
|
+
color: 'white',
|
|
60
|
+
}, children: unreadCount }))] })), _jsx("div", { style: contentStyle, children: isLoading ? (_jsx("div", { style: { padding: 40, textAlign: 'center' }, children: _jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { animation: 'spin 1s linear infinite' }, children: [_jsx("style", { children: `@keyframes spin{to{transform:rotate(360deg)}}` }), _jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })] }) })) : changes.length === 0 ? (_jsx("div", { style: { padding: 40, textAlign: 'center', color: '#64748b' }, children: _jsx("p", { children: "No release notes yet. Check back soon!" }) })) : (changes.map((change) => (_jsxs("article", { style: { padding: '16px 0', borderBottom: '1px solid var(--border, #e2e8f0)' }, children: [_jsxs("div", { style: {
|
|
61
|
+
display: 'flex',
|
|
62
|
+
alignItems: 'flex-start',
|
|
63
|
+
justifyContent: 'space-between',
|
|
64
|
+
gap: 12,
|
|
65
|
+
marginBottom: 4,
|
|
66
|
+
}, children: [_jsx("h3", { style: { margin: 0, fontSize: 14, fontWeight: 600 }, children: change.changeName }), config.showStatus && (_jsx("span", { style: {
|
|
67
|
+
fontSize: 11,
|
|
68
|
+
fontWeight: 500,
|
|
69
|
+
padding: '2px 8px',
|
|
70
|
+
borderRadius: 12,
|
|
71
|
+
background: 'rgba(59, 130, 246, 0.1)',
|
|
72
|
+
color: config.primaryColor || '#3b82f6',
|
|
73
|
+
border: `1px solid ${config.primaryColor || '#3b82f6'}`,
|
|
74
|
+
whiteSpace: 'nowrap',
|
|
75
|
+
}, children: change.currentStatus }))] }), _jsx("p", { style: {
|
|
76
|
+
margin: '4px 0',
|
|
77
|
+
fontSize: 13,
|
|
78
|
+
color: '#64748b',
|
|
79
|
+
lineHeight: 1.4,
|
|
80
|
+
}, children: change.changeSummary }), _jsx("div", { style: { margin: '8px 0 0', fontSize: 13, lineHeight: 1.5 }, dangerouslySetInnerHTML: {
|
|
81
|
+
__html: renderDescription(change.changeDescription),
|
|
82
|
+
} }), config.showDates && (_jsx("div", { style: { marginTop: 8, fontSize: 11, color: '#64748b' }, children: formatChangeDate(change._creationTime) }))] }, change._id)))) }), showFooter && !isLoading && changes.length > 0 && (_jsxs("div", { style: footerStyle, children: [_jsx("button", { onClick: handleMarkAsRead, disabled: unreadCount === 0, style: {
|
|
83
|
+
fontSize: 13,
|
|
84
|
+
padding: '6px 12px',
|
|
85
|
+
borderRadius: 6,
|
|
86
|
+
border: 'none',
|
|
87
|
+
background: config.primaryColor || '#3b82f6',
|
|
88
|
+
color: 'white',
|
|
89
|
+
cursor: unreadCount === 0 ? 'default' : 'pointer',
|
|
90
|
+
fontWeight: 500,
|
|
91
|
+
opacity: unreadCount === 0 ? 0.5 : 1,
|
|
92
|
+
}, children: unreadCount === 0 ? 'All caught up' : 'Mark all as read' }), _jsx("a", { href: releaseNotesUrl, target: "_blank", rel: "noopener", style: {
|
|
93
|
+
fontSize: 13,
|
|
94
|
+
color: config.primaryColor || '#3b82f6',
|
|
95
|
+
textDecoration: 'none',
|
|
96
|
+
fontWeight: 500,
|
|
97
|
+
}, children: "View all \u2192" })] }))] }));
|
|
98
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { WidgetConfig } from './types';
|
|
3
|
+
export interface RlseWidgetMenuProps extends WidgetConfig {
|
|
4
|
+
/** Control open state externally */
|
|
5
|
+
isOpen?: boolean;
|
|
6
|
+
/** Callback when menu should close */
|
|
7
|
+
onClose?: () => void;
|
|
8
|
+
/** Container style for the dropdown */
|
|
9
|
+
dropdownStyle?: React.CSSProperties;
|
|
10
|
+
/** Max height of the dropdown */
|
|
11
|
+
maxHeight?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function RlseWidgetMenu(props: RlseWidgetMenuProps): React.ReactElement | null;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useState } from 'react';
|
|
3
|
+
import { formatChangeDate, getReleaseNotesUrl } from '@rlse/widget-core';
|
|
4
|
+
import { DEFAULT_CONFIG } from './types';
|
|
5
|
+
import { useWidgetData } from './useWidgetData';
|
|
6
|
+
import { markAllChangesAsSeen, getUnreadCount } from './storage';
|
|
7
|
+
export function RlseWidgetMenu(props) {
|
|
8
|
+
const config = { ...DEFAULT_CONFIG, ...props };
|
|
9
|
+
const { isOpen: controlledIsOpen, onClose, dropdownStyle, maxHeight = 400, } = props;
|
|
10
|
+
// Internal state if not controlled externally
|
|
11
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
12
|
+
const isOpen = controlledIsOpen ?? internalOpen;
|
|
13
|
+
const { changes, isLoading } = useWidgetData(config.orgSlug, config.appSlug, config.limit, config.baseUrl);
|
|
14
|
+
const handleClose = useCallback(() => {
|
|
15
|
+
if (onClose) {
|
|
16
|
+
onClose();
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
setInternalOpen(false);
|
|
20
|
+
}
|
|
21
|
+
}, [onClose]);
|
|
22
|
+
const handleMarkAsRead = useCallback(() => {
|
|
23
|
+
const changeIds = changes.map((c) => c._id);
|
|
24
|
+
markAllChangesAsSeen(config.orgSlug, changeIds);
|
|
25
|
+
window.dispatchEvent(new Event('storage'));
|
|
26
|
+
handleClose();
|
|
27
|
+
}, [changes, config.orgSlug, handleClose]);
|
|
28
|
+
const unreadCount = getUnreadCount(config.orgSlug, changes);
|
|
29
|
+
const releaseNotesUrl = getReleaseNotesUrl(config.baseUrl, config.orgSlug, config.appSlug);
|
|
30
|
+
const defaultDropdownStyle = {
|
|
31
|
+
position: 'absolute',
|
|
32
|
+
top: '100%',
|
|
33
|
+
right: 0,
|
|
34
|
+
marginTop: 8,
|
|
35
|
+
background: 'var(--background, white)',
|
|
36
|
+
borderRadius: 12,
|
|
37
|
+
border: '1px solid var(--border, #e2e8f0)',
|
|
38
|
+
boxShadow: '0 10px 40px -10px rgba(0, 0, 0, 0.2)',
|
|
39
|
+
width: 320,
|
|
40
|
+
maxHeight,
|
|
41
|
+
display: 'flex',
|
|
42
|
+
flexDirection: 'column',
|
|
43
|
+
overflow: 'hidden',
|
|
44
|
+
zIndex: 1000,
|
|
45
|
+
};
|
|
46
|
+
const headerStyle = {
|
|
47
|
+
display: 'flex',
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
justifyContent: 'space-between',
|
|
50
|
+
padding: '12px 16px',
|
|
51
|
+
borderBottom: '1px solid var(--border, #e2e8f0)',
|
|
52
|
+
};
|
|
53
|
+
const titleStyle = {
|
|
54
|
+
margin: 0,
|
|
55
|
+
fontSize: 14,
|
|
56
|
+
fontWeight: 600,
|
|
57
|
+
};
|
|
58
|
+
const contentStyle = {
|
|
59
|
+
overflowY: 'auto',
|
|
60
|
+
padding: '0 16px',
|
|
61
|
+
flex: 1,
|
|
62
|
+
};
|
|
63
|
+
const footerStyle = {
|
|
64
|
+
display: 'flex',
|
|
65
|
+
alignItems: 'center',
|
|
66
|
+
justifyContent: 'space-between',
|
|
67
|
+
padding: '10px 16px',
|
|
68
|
+
borderTop: '1px solid var(--border, #e2e8f0)',
|
|
69
|
+
};
|
|
70
|
+
return (_jsxs("div", { style: { position: 'relative' }, children: [_jsxs("button", { onClick: () => setInternalOpen(!internalOpen), style: {
|
|
71
|
+
display: 'flex',
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
gap: 8,
|
|
74
|
+
padding: '8px 12px',
|
|
75
|
+
borderRadius: 8,
|
|
76
|
+
border: 'none',
|
|
77
|
+
background: 'transparent',
|
|
78
|
+
cursor: 'pointer',
|
|
79
|
+
fontSize: 14,
|
|
80
|
+
fontWeight: 500,
|
|
81
|
+
color: 'inherit',
|
|
82
|
+
}, children: [_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275z" }), _jsx("path", { d: "M5 3v4" }), _jsx("path", { d: "M19 17v4" }), _jsx("path", { d: "M3 5h4" }), _jsx("path", { d: "M17 19h4" })] }), config.triggerLabel, unreadCount > 0 && (_jsx("span", { style: {
|
|
83
|
+
fontSize: 11,
|
|
84
|
+
fontWeight: 600,
|
|
85
|
+
minWidth: 18,
|
|
86
|
+
height: 18,
|
|
87
|
+
padding: '0 5px',
|
|
88
|
+
borderRadius: 9,
|
|
89
|
+
background: config.primaryColor || '#3b82f6',
|
|
90
|
+
color: 'white',
|
|
91
|
+
display: 'flex',
|
|
92
|
+
alignItems: 'center',
|
|
93
|
+
justifyContent: 'center',
|
|
94
|
+
}, children: unreadCount > 99 ? '99+' : unreadCount }))] }), isOpen && (_jsxs(_Fragment, { children: [_jsx("div", { onClick: handleClose, style: {
|
|
95
|
+
position: 'fixed',
|
|
96
|
+
inset: 0,
|
|
97
|
+
zIndex: 999,
|
|
98
|
+
} }), _jsxs("div", { style: { ...defaultDropdownStyle, ...dropdownStyle }, children: [_jsxs("div", { style: headerStyle, children: [_jsx("h2", { style: titleStyle, children: config.modalTitle }), _jsx("button", { onClick: handleClose, style: {
|
|
99
|
+
background: 'none',
|
|
100
|
+
border: 'none',
|
|
101
|
+
cursor: 'pointer',
|
|
102
|
+
padding: 4,
|
|
103
|
+
display: 'flex',
|
|
104
|
+
alignItems: 'center',
|
|
105
|
+
justifyContent: 'center',
|
|
106
|
+
color: '#64748b',
|
|
107
|
+
}, children: _jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M18 6 6 18" }), _jsx("path", { d: "m6 6 12 12" })] }) })] }), _jsx("div", { style: contentStyle, children: isLoading ? (_jsx("div", { style: { padding: 32, textAlign: 'center' }, children: _jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { animation: 'spin 1s linear infinite' }, children: [_jsx("style", { children: `@keyframes spin{to{transform:rotate(360deg)}}` }), _jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })] }) })) : changes.length === 0 ? (_jsx("div", { style: {
|
|
108
|
+
padding: 32,
|
|
109
|
+
textAlign: 'center',
|
|
110
|
+
color: '#64748b',
|
|
111
|
+
fontSize: 13,
|
|
112
|
+
}, children: _jsx("p", { children: "No release notes yet." }) })) : (changes.map((change) => (_jsxs("article", { style: {
|
|
113
|
+
padding: '12px 0',
|
|
114
|
+
borderBottom: '1px solid var(--border, #e2e8f0)',
|
|
115
|
+
}, children: [_jsxs("div", { style: {
|
|
116
|
+
display: 'flex',
|
|
117
|
+
alignItems: 'center',
|
|
118
|
+
justifyContent: 'space-between',
|
|
119
|
+
gap: 8,
|
|
120
|
+
marginBottom: 2,
|
|
121
|
+
}, children: [_jsx("h3", { style: { margin: 0, fontSize: 13, fontWeight: 600 }, children: change.changeName }), config.showStatus && (_jsx("span", { style: {
|
|
122
|
+
fontSize: 10,
|
|
123
|
+
fontWeight: 500,
|
|
124
|
+
padding: '1px 6px',
|
|
125
|
+
borderRadius: 10,
|
|
126
|
+
background: 'rgba(59, 130, 246, 0.1)',
|
|
127
|
+
color: config.primaryColor || '#3b82f6',
|
|
128
|
+
border: `1px solid ${config.primaryColor || '#3b82f6'}`,
|
|
129
|
+
whiteSpace: 'nowrap',
|
|
130
|
+
}, children: change.currentStatus }))] }), _jsx("p", { style: {
|
|
131
|
+
margin: '2px 0',
|
|
132
|
+
fontSize: 12,
|
|
133
|
+
color: '#64748b',
|
|
134
|
+
lineHeight: 1.4,
|
|
135
|
+
}, children: change.changeSummary }), config.showDates && (_jsx("div", { style: { marginTop: 4, fontSize: 10, color: '#94a3b8' }, children: formatChangeDate(change._creationTime) }))] }, change._id)))) }), !isLoading && changes.length > 0 && (_jsxs("div", { style: footerStyle, children: [_jsx("button", { onClick: handleMarkAsRead, disabled: unreadCount === 0, style: {
|
|
136
|
+
fontSize: 12,
|
|
137
|
+
padding: '5px 10px',
|
|
138
|
+
borderRadius: 6,
|
|
139
|
+
border: 'none',
|
|
140
|
+
background: config.primaryColor || '#3b82f6',
|
|
141
|
+
color: 'white',
|
|
142
|
+
cursor: unreadCount === 0 ? 'default' : 'pointer',
|
|
143
|
+
fontWeight: 500,
|
|
144
|
+
opacity: unreadCount === 0 ? 0.5 : 1,
|
|
145
|
+
}, children: unreadCount === 0 ? 'All caught up' : 'Mark read' }), _jsx("a", { href: releaseNotesUrl, target: "_blank", rel: "noopener", style: {
|
|
146
|
+
fontSize: 12,
|
|
147
|
+
color: config.primaryColor || '#3b82f6',
|
|
148
|
+
textDecoration: 'none',
|
|
149
|
+
fontWeight: 500,
|
|
150
|
+
}, children: "View all \u2192" })] }))] })] }))] }));
|
|
151
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { OnInit, OnDestroy } from '@angular/core';
|
|
2
|
+
import { WidgetDataService } from '../services/widget-data.service';
|
|
3
|
+
import type { WidgetConfig, Change } from '@rlse/widget-core';
|
|
4
|
+
export declare class RlseWidgetComponent implements OnInit, OnDestroy {
|
|
5
|
+
private widgetDataService;
|
|
6
|
+
orgSlug: string;
|
|
7
|
+
appSlug?: string;
|
|
8
|
+
trigger: 'auto' | 'manual' | 'both';
|
|
9
|
+
limit: number;
|
|
10
|
+
showStatus: boolean;
|
|
11
|
+
showDates: boolean;
|
|
12
|
+
position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
13
|
+
theme: 'light' | 'dark' | 'auto';
|
|
14
|
+
primaryColor?: string;
|
|
15
|
+
triggerLabel: string;
|
|
16
|
+
modalTitle: string;
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
config: Required<WidgetConfig>;
|
|
19
|
+
changes: Change[];
|
|
20
|
+
isLoading: boolean;
|
|
21
|
+
isOpen: boolean;
|
|
22
|
+
unreadCount: number;
|
|
23
|
+
releaseNotesUrl: string;
|
|
24
|
+
private destroy$;
|
|
25
|
+
constructor(widgetDataService: WidgetDataService);
|
|
26
|
+
ngOnInit(): void;
|
|
27
|
+
ngOnDestroy(): void;
|
|
28
|
+
private loadChanges;
|
|
29
|
+
markAsRead(): void;
|
|
30
|
+
onOverlayClick(event: MouseEvent): void;
|
|
31
|
+
renderDescription(content: string): string;
|
|
32
|
+
formatDate(timestamp: number): string;
|
|
33
|
+
get triggerStyle(): string;
|
|
34
|
+
get badgeStyle(): string;
|
|
35
|
+
get overlayStyle(): string;
|
|
36
|
+
get modalStyle(): string;
|
|
37
|
+
get headerStyle(): string;
|
|
38
|
+
get titleStyle(): string;
|
|
39
|
+
get closeButtonStyle(): string;
|
|
40
|
+
get contentStyle(): string;
|
|
41
|
+
get centeredStyle(): string;
|
|
42
|
+
get emptyStyle(): string;
|
|
43
|
+
get changeCardStyle(): string;
|
|
44
|
+
get changeHeaderStyle(): string;
|
|
45
|
+
get changeTitleStyle(): string;
|
|
46
|
+
get statusBadgeStyle(): string;
|
|
47
|
+
get summaryStyle(): string;
|
|
48
|
+
get descriptionStyle(): string;
|
|
49
|
+
get dateStyle(): string;
|
|
50
|
+
get footerStyle(): string;
|
|
51
|
+
get markReadButtonStyle(): string;
|
|
52
|
+
get viewAllLinkStyle(): string;
|
|
53
|
+
}
|