@rlse/widget 0.1.5 → 0.2.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.
package/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # @rlse/widget
2
2
 
3
- React component for embedding [![](https://www.google.com/s2/favicons?domain=rlse.dev&sz=16) rlse.dev](https://rlse.dev) release notes directly in your application.
3
+ Multi-framework release notes widget for [![](https://www.google.com/s2/favicons?domain=rlse.dev&sz=16) rlse.dev](https://rlse.dev). Works with React, Vue, Angular, Svelte, Astro, and vanilla JavaScript.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@rlse/widget)](https://www.npmjs.com/package/@rlse/widget)
6
6
  [![Socket Badge](https://badge.socket.dev/npm/package/@rlse/widget/latest)](https://socket.dev/npm/package/@rlse/widget)
7
7
 
8
8
  ## Features
9
9
 
10
+ - **Multi-framework**: React, Vue, Angular, Svelte, Astro, Vanilla JS
11
+ - **Three variants**: Floating button, embedded panel, dropdown menu
10
12
  - **Smart triggers**: Manual button, auto-popup on new releases, or both
11
13
  - **Unread badge**: Shows count of unseen changes
12
14
  - **App filtering**: Show changes for a specific application or all
@@ -21,6 +23,8 @@ npm install @rlse/widget
21
23
 
22
24
  ## Quick Start
23
25
 
26
+ ### React / Next.js
27
+
24
28
  ```tsx
25
29
  import { RlseWidget } from '@rlse/widget';
26
30
 
@@ -34,6 +38,60 @@ function App() {
34
38
  }
35
39
  ```
36
40
 
41
+ ### Vue / Nuxt
42
+
43
+ ```vue
44
+ <script setup>
45
+ import { RlseWidget } from '@rlse/widget/vue';
46
+ </script>
47
+
48
+ <template>
49
+ <RlseWidget org-slug="your-org-slug" />
50
+ </template>
51
+ ```
52
+
53
+ ### Angular
54
+
55
+ ```ts
56
+ import { RlseWidgetComponent } from '@rlse/widget/angular';
57
+
58
+ @Component({
59
+ imports: [RlseWidgetComponent],
60
+ template: `<rlse-widget orgSlug="your-org-slug"></rlse-widget>`
61
+ })
62
+ ```
63
+
64
+ ### Svelte / SvelteKit
65
+
66
+ ```svelte
67
+ <script>
68
+ import { RlseWidget } from '@rlse/widget/svelte';
69
+ </script>
70
+
71
+ <RlseWidget orgSlug="your-org-slug" />
72
+ ```
73
+
74
+ ### Astro
75
+
76
+ ```astro
77
+ ---
78
+ import { RlseWidget } from '@rlse/widget/react'; // or vue/svelte
79
+ ---
80
+
81
+ <RlseWidget orgSlug="your-org-slug" client:load />
82
+ ```
83
+
84
+ ### Vanilla JS (CDN)
85
+
86
+ ```html
87
+ <script>
88
+ window.rlseWidgetConfig = {
89
+ orgSlug: 'your-org-slug',
90
+ };
91
+ </script>
92
+ <script async src="https://rlse.dev/widget.js"></script>
93
+ ```
94
+
37
95
  ## Configuration
38
96
 
39
97
  ### Required
@@ -56,9 +114,15 @@ function App() {
56
114
  | `primaryColor` | `string` | `#3b82f6` | Accent color (hex) |
57
115
  | `autoShowAfter` | `number` | `7` | Days before auto-popup triggers again |
58
116
 
59
- ## Full Example
117
+ ## Widget Variants
118
+
119
+ ### RlseWidget (Floating Button)
120
+
121
+ A floating trigger button that opens a modal. Best for standalone pages.
60
122
 
61
123
  ```tsx
124
+ import { RlseWidget } from '@rlse/widget';
125
+
62
126
  <RlseWidget
63
127
  orgSlug="acme-corp"
64
128
  appSlug="dashboard"
@@ -66,13 +130,43 @@ function App() {
66
130
  position="bottom-right"
67
131
  theme="auto"
68
132
  limit={5}
69
- triggerLabel="🎁 What's New"
70
- modalTitle="Acme Release Notes"
71
- primaryColor="#10b981"
72
- autoShowAfter={3}
73
- />
133
+ triggerLabel="What's New"
134
+ modalTitle="Release Notes"
135
+ primaryColor="#3b82f6"
136
+ />;
137
+ ```
138
+
139
+ ### RlseWidgetEmbed (Inline Panel)
140
+
141
+ An embeddable panel for sidebars or dedicated pages. Great for dashboards.
142
+
143
+ ```tsx
144
+ import { RlseWidgetEmbed } from '@rlse/widget';
145
+
146
+ <RlseWidgetEmbed
147
+ orgSlug="acme-corp"
148
+ appSlug="dashboard"
149
+ showHeader={true}
150
+ showFooter={true}
151
+ />;
74
152
  ```
75
153
 
154
+ ### RlseWidgetMenu (Dropdown)
155
+
156
+ A compact dropdown for navigation bars. Perfect for user menus.
157
+
158
+ ```tsx
159
+ import { RlseWidgetMenu } from '@rlse/widget';
160
+
161
+ <RlseWidgetMenu
162
+ orgSlug="acme-corp"
163
+ appSlug="dashboard"
164
+ triggerLabel="What's New"
165
+ />;
166
+ ```
167
+
168
+ ````
169
+
76
170
  ## Trigger Modes
77
171
 
78
172
  - **`manual`** — Floating button always visible; click to open. Unread badge shows unseen count.
@@ -93,7 +187,7 @@ Use `primaryColor` to match your brand:
93
187
 
94
188
  ```tsx
95
189
  <RlseWidget orgSlug="acme-corp" primaryColor="#ff6b6b" />
96
- ```
190
+ ````
97
191
 
98
192
  ## Troubleshooting
99
193
 
@@ -119,3 +213,4 @@ Use `primaryColor` to match your brand:
119
213
 
120
214
  - Email: support@rlse.dev
121
215
  - Dashboard: Settings → Widget Embed
216
+ - Full documentation: https://rlse.dev/docs/widget
@@ -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, '&amp;')
41
- .replace(/</g, '&lt;')
42
- .replace(/>/g, '&gt;')
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
+ }