@rlse/widget 0.2.1 → 0.2.2

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.
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback } from 'react';
3
- import { renderDescription, formatChangeDate, getReleaseNotesUrl } from '@rlse/widget-core';
3
+ import { renderDescription, formatChangeDate, getReleaseNotesUrl, } from '@rlse/widget-core';
4
4
  import { DEFAULT_CONFIG } from './types';
5
5
  import { useWidgetData } from './useWidgetData';
6
6
  import { markAllChangesAsSeen, getUnreadCount } from './storage';
@@ -57,7 +57,10 @@ export function RlseWidgetEmbed(props) {
57
57
  borderRadius: 10,
58
58
  background: config.primaryColor || '#3b82f6',
59
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: {
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: {
61
+ padding: '16px 0',
62
+ borderBottom: '1px solid var(--border, #e2e8f0)',
63
+ }, children: [_jsxs("div", { style: {
61
64
  display: 'flex',
62
65
  alignItems: 'flex-start',
63
66
  justifyContent: 'space-between',
@@ -1,4 +1,4 @@
1
1
  export { RlseWidgetComponent } from './components/rlse-widget.component';
2
- export { WidgetDataService, type WidgetDataState } from './services/widget-data.service';
2
+ export { WidgetDataService, type WidgetDataState, } from './services/widget-data.service';
3
3
  export type { WidgetConfig, Change, WidgetChangesResponse, } from '@rlse/widget-core';
4
4
  export { DEFAULT_CONFIG } from '@rlse/widget-core';
@@ -1,5 +1,5 @@
1
1
  // Angular Components
2
2
  export { RlseWidgetComponent } from './components/rlse-widget.component';
3
3
  // Angular Services
4
- export { WidgetDataService } from './services/widget-data.service';
4
+ export { WidgetDataService, } from './services/widget-data.service';
5
5
  export { DEFAULT_CONFIG } from '@rlse/widget-core';
@@ -0,0 +1,19 @@
1
+ import type { WidgetChangesResponse } from './types';
2
+ export interface FetchChangesParams {
3
+ orgSlug: string;
4
+ appSlug?: string;
5
+ limit: number;
6
+ baseUrl: string;
7
+ signal?: AbortSignal;
8
+ }
9
+ export interface FetchChangesResult {
10
+ changes: WidgetChangesResponse['changes'];
11
+ error: Error | null;
12
+ }
13
+ export declare function fetchChanges({ orgSlug, appSlug, limit, baseUrl, signal, }: FetchChangesParams): Promise<FetchChangesResult>;
14
+ export declare class WidgetDataFetcher {
15
+ private abortController;
16
+ fetch(params: Omit<FetchChangesParams, 'signal'>): Promise<FetchChangesResult>;
17
+ abort(): void;
18
+ destroy(): void;
19
+ }
@@ -0,0 +1,64 @@
1
+ export async function fetchChanges({ orgSlug, appSlug, limit, baseUrl, signal, }) {
2
+ try {
3
+ // Validate baseUrl
4
+ if (!baseUrl || typeof baseUrl !== 'string') {
5
+ throw new Error(`Invalid baseUrl: ${baseUrl}`);
6
+ }
7
+ let url;
8
+ try {
9
+ url = new URL(`${baseUrl}/api/widget/changes`);
10
+ }
11
+ catch {
12
+ throw new Error(`Invalid baseUrl for widget API: ${baseUrl}`);
13
+ }
14
+ url.searchParams.set('orgSlug', orgSlug);
15
+ if (appSlug) {
16
+ url.searchParams.set('appSlug', appSlug);
17
+ }
18
+ url.searchParams.set('limit', limit.toString());
19
+ const response = await fetch(url.toString(), { signal });
20
+ if (!response.ok) {
21
+ throw new Error(`Failed to fetch changes: ${response.status}`);
22
+ }
23
+ const data = await response.json();
24
+ return { changes: data.changes, error: null };
25
+ }
26
+ catch (err) {
27
+ // Don't return error if request was aborted
28
+ if (err instanceof Error && err.name === 'AbortError') {
29
+ return { changes: [], error: null };
30
+ }
31
+ return {
32
+ changes: [],
33
+ error: err instanceof Error ? err : new Error('Unknown error'),
34
+ };
35
+ }
36
+ }
37
+ export class WidgetDataFetcher {
38
+ constructor() {
39
+ this.abortController = null;
40
+ }
41
+ async fetch(params) {
42
+ // Cancel any in-flight request
43
+ this.abort();
44
+ this.abortController = new AbortController();
45
+ const result = await fetchChanges({
46
+ ...params,
47
+ signal: this.abortController.signal,
48
+ });
49
+ // Clear controller after successful completion
50
+ if (this.abortController?.signal.aborted === false) {
51
+ this.abortController = null;
52
+ }
53
+ return result;
54
+ }
55
+ abort() {
56
+ if (this.abortController) {
57
+ this.abortController.abort();
58
+ this.abortController = null;
59
+ }
60
+ }
61
+ destroy() {
62
+ this.abort();
63
+ }
64
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Render a change description.
3
+ * - HTML content (new format): passed through after stripping event handlers
4
+ * and javascript: URLs as a safety net
5
+ * - Markdown content (legacy records): converted via simpleMarkdownToHtml
6
+ */
7
+ export declare function renderDescription(content: string): string;
8
+ export declare function escapeHtml(text: string): string;
9
+ export declare function formatChangeDate(timestamp: number): string;
10
+ export declare function getReleaseNotesUrl(baseUrl: string, orgSlug: string, appSlug?: string): string;
@@ -0,0 +1,92 @@
1
+ // No external imports needed
2
+ /**
3
+ * Render a change description.
4
+ * - HTML content (new format): passed through after stripping event handlers
5
+ * and javascript: URLs as a safety net
6
+ * - Markdown content (legacy records): converted via simpleMarkdownToHtml
7
+ */
8
+ export function renderDescription(content) {
9
+ if (!content)
10
+ return '';
11
+ if (content.trimStart().startsWith('<')) {
12
+ // HTML content: strip on* attributes and javascript: URLs
13
+ return content
14
+ .replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi, '')
15
+ .replace(/href\s*=\s*"javascript:[^"]*"/gi, '')
16
+ .replace(/href\s*=\s*'javascript:[^']*'/gi, '');
17
+ }
18
+ return simpleMarkdownToHtml(content);
19
+ }
20
+ function simpleMarkdownToHtml(markdown) {
21
+ // Helper to sanitize URLs - only allow safe protocols
22
+ const isSafeUrl = (url) => {
23
+ try {
24
+ const parsed = new URL(url);
25
+ const protocol = parsed.protocol.replace(':', '');
26
+ return ['http', 'https', 'mailto', 'tel'].includes(protocol);
27
+ }
28
+ catch {
29
+ // Relative URLs are OK
30
+ return (url.startsWith('/') ||
31
+ url.startsWith('./') ||
32
+ url.startsWith('../') ||
33
+ !url.includes(':'));
34
+ }
35
+ };
36
+ return (markdown
37
+ // Escape HTML
38
+ .replace(/&/g, '&amp;')
39
+ .replace(/</g, '&lt;')
40
+ .replace(/>/g, '&gt;')
41
+ // Bold
42
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
43
+ .replace(/__(.+?)__/g, '<strong>$1</strong>')
44
+ // Italic
45
+ .replace(/\*(.+?)\*/g, '<em>$1</em>')
46
+ .replace(/_(.+?)_/g, '<em>$1</em>')
47
+ // Code
48
+ .replace(/`(.+?)`/g, '<code style="background:var(--rlse-border);padding:2px 4px;border-radius:4px;font-family:monospace;font-size:90%;">$1</code>')
49
+ // Links - with URL validation
50
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, url) => {
51
+ if (isSafeUrl(url)) {
52
+ // Escape both url and text to prevent attribute injection and XSS
53
+ const safeUrl = url
54
+ .replace(/&/g, '&amp;')
55
+ .replace(/"/g, '&quot;')
56
+ .replace(/</g, '&lt;')
57
+ .replace(/>/g, '&gt;');
58
+ return `<a href="${safeUrl}" target="_blank" rel="noopener" style="color:var(--rlse-accent);text-decoration:none;">${text}</a>`;
59
+ }
60
+ return text.replace(/</g, '&lt;').replace(/>/g, '&gt;'); // Return escaped plain text for unsafe URLs
61
+ })
62
+ // Lists
63
+ .replace(/^- (.+)$/gm, '<li>$1</li>')
64
+ // Paragraphs
65
+ .split('\n\n')
66
+ .map((para) => para.startsWith('<li>') ? `<ul>${para}</ul>` : `<p>${para}</p>`)
67
+ .join(''));
68
+ }
69
+ export function escapeHtml(text) {
70
+ const div = document.createElement('div');
71
+ div.textContent = text;
72
+ return div.innerHTML;
73
+ }
74
+ export function formatChangeDate(timestamp) {
75
+ return new Date(timestamp).toLocaleDateString('en-US', {
76
+ year: 'numeric',
77
+ month: 'short',
78
+ day: 'numeric',
79
+ });
80
+ }
81
+ export function getReleaseNotesUrl(baseUrl, orgSlug, appSlug) {
82
+ try {
83
+ const parsed = new URL(baseUrl);
84
+ const base = `${parsed.protocol}//${orgSlug}.${parsed.host}`;
85
+ return appSlug ? `${base}/${appSlug}` : base;
86
+ }
87
+ catch {
88
+ return appSlug
89
+ ? `${baseUrl}/${orgSlug}/${appSlug}`
90
+ : `${baseUrl}/${orgSlug}`;
91
+ }
92
+ }
package/dist/storage.d.ts CHANGED
@@ -1 +1,11 @@
1
- export { getStorageKey, getLastVisitKey, getSeenChangesKey, getLastVisit, setLastVisit, getSeenChanges, markAllChangesAsSeen, getUnreadCount, shouldAutoShow, } from '@rlse/widget-core';
1
+ export declare function getStorageKey(orgSlug: string): string;
2
+ export declare function getLastVisitKey(orgSlug: string): string;
3
+ export declare function getSeenChangesKey(orgSlug: string): string;
4
+ export declare function getLastVisit(orgSlug: string): number | null;
5
+ export declare function setLastVisit(orgSlug: string): void;
6
+ export declare function getSeenChanges(orgSlug: string): string[];
7
+ export declare function markAllChangesAsSeen(orgSlug: string, changeIds: string[]): void;
8
+ export declare function getUnreadCount(orgSlug: string, changes: {
9
+ _id: string;
10
+ }[]): number;
11
+ export declare function shouldAutoShow(orgSlug: string, autoShowAfter: number): boolean;
package/dist/storage.js CHANGED
@@ -1,2 +1,80 @@
1
- // Re-export all storage functions from core
2
- export { getStorageKey, getLastVisitKey, getSeenChangesKey, getLastVisit, setLastVisit, getSeenChanges, markAllChangesAsSeen, getUnreadCount, shouldAutoShow, } from '@rlse/widget-core';
1
+ const STORAGE_KEY_PREFIX = 'rlse_widget_';
2
+ const LAST_VISIT_KEY_SUFFIX = '_last_visit';
3
+ const SEEN_CHANGES_KEY_SUFFIX = '_seen';
4
+ export function getStorageKey(orgSlug) {
5
+ return `${STORAGE_KEY_PREFIX}${orgSlug}`;
6
+ }
7
+ export function getLastVisitKey(orgSlug) {
8
+ return `${getStorageKey(orgSlug)}${LAST_VISIT_KEY_SUFFIX}`;
9
+ }
10
+ export function getSeenChangesKey(orgSlug) {
11
+ return `${getStorageKey(orgSlug)}${SEEN_CHANGES_KEY_SUFFIX}`;
12
+ }
13
+ export function getLastVisit(orgSlug) {
14
+ if (typeof window === 'undefined')
15
+ return null;
16
+ try {
17
+ const value = localStorage.getItem(getLastVisitKey(orgSlug));
18
+ if (!value)
19
+ return null;
20
+ const parsed = parseInt(value, 10);
21
+ return Number.isNaN(parsed) ? null : parsed;
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ export function setLastVisit(orgSlug) {
28
+ if (typeof window === 'undefined')
29
+ return;
30
+ try {
31
+ localStorage.setItem(getLastVisitKey(orgSlug), Date.now().toString());
32
+ }
33
+ catch {
34
+ // Ignore storage errors
35
+ }
36
+ }
37
+ export function getSeenChanges(orgSlug) {
38
+ if (typeof window === 'undefined')
39
+ return [];
40
+ try {
41
+ const value = localStorage.getItem(getSeenChangesKey(orgSlug));
42
+ if (!value)
43
+ return [];
44
+ const parsed = JSON.parse(value);
45
+ // Validate that parsed is an array of strings
46
+ if (!Array.isArray(parsed) ||
47
+ !parsed.every((item) => typeof item === 'string')) {
48
+ return [];
49
+ }
50
+ return parsed;
51
+ }
52
+ catch {
53
+ return [];
54
+ }
55
+ }
56
+ export function markAllChangesAsSeen(orgSlug, changeIds) {
57
+ if (typeof window === 'undefined')
58
+ return;
59
+ try {
60
+ localStorage.setItem(getSeenChangesKey(orgSlug), JSON.stringify(changeIds));
61
+ }
62
+ catch {
63
+ // Ignore storage errors
64
+ }
65
+ }
66
+ export function getUnreadCount(orgSlug, changes) {
67
+ if (typeof window === 'undefined')
68
+ return 0;
69
+ const seen = getSeenChanges(orgSlug);
70
+ return changes.filter((c) => !seen.includes(c._id)).length;
71
+ }
72
+ export function shouldAutoShow(orgSlug, autoShowAfter) {
73
+ if (typeof window === 'undefined')
74
+ return false;
75
+ const lastVisit = getLastVisit(orgSlug);
76
+ if (!lastVisit)
77
+ return true; // First visit
78
+ const daysSinceVisit = (Date.now() - lastVisit) / (1000 * 60 * 60 * 24);
79
+ return daysSinceVisit >= autoShowAfter;
80
+ }
package/dist/types.d.ts CHANGED
@@ -1,2 +1,54 @@
1
- export type { WidgetConfig, Change, WidgetChangesResponse, } from '@rlse/widget-core';
2
- export { DEFAULT_CONFIG } from '@rlse/widget-core';
1
+ export interface WidgetConfig {
2
+ /** Organization slug (required) */
3
+ orgSlug: string;
4
+ /** Optional app slug to filter changes */
5
+ appSlug?: string;
6
+ /** Trigger behavior: 'auto', 'manual', or 'both' */
7
+ trigger?: 'auto' | 'manual' | 'both';
8
+ /** Days since last visit to auto-show (for 'auto' or 'both') */
9
+ autoShowAfter?: number;
10
+ /** Maximum changes to display */
11
+ limit?: number;
12
+ /** Show status badges */
13
+ showStatus?: boolean;
14
+ /** Show creation dates */
15
+ showDates?: boolean;
16
+ /** Button position for manual trigger (floating widget only) */
17
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
18
+ /** Theme: 'light', 'dark', or 'auto' */
19
+ theme?: 'light' | 'dark' | 'auto';
20
+ /** Primary accent color (hex) */
21
+ primaryColor?: string;
22
+ /** Button text (floating widget only) */
23
+ triggerLabel?: string;
24
+ /** Modal/Panel title */
25
+ modalTitle?: string;
26
+ /** Base URL for the server (optional, defaults to https://rlse.dev) */
27
+ baseUrl?: string;
28
+ }
29
+ export interface Change {
30
+ _id: string;
31
+ _creationTime: number;
32
+ changeName: string;
33
+ changeSummary: string;
34
+ changeDescription: string;
35
+ currentStatus: string;
36
+ appName?: string;
37
+ appSlug?: string;
38
+ }
39
+ export interface WidgetChangesResponse {
40
+ org: {
41
+ orgName: string;
42
+ orgSlug: string;
43
+ };
44
+ app?: {
45
+ appName: string;
46
+ appSlug: string;
47
+ };
48
+ changes: Change[];
49
+ meta: {
50
+ total: number;
51
+ limit: number;
52
+ };
53
+ }
54
+ export declare const DEFAULT_CONFIG: Partial<WidgetConfig>;
package/dist/types.js CHANGED
@@ -1 +1,12 @@
1
- export { DEFAULT_CONFIG } from '@rlse/widget-core';
1
+ export const DEFAULT_CONFIG = {
2
+ trigger: 'manual',
3
+ limit: 10,
4
+ showStatus: true,
5
+ showDates: true,
6
+ position: 'bottom-right',
7
+ theme: 'auto',
8
+ triggerLabel: "What's New",
9
+ modalTitle: 'Release Notes',
10
+ baseUrl: 'https://rlse.dev',
11
+ autoShowAfter: 7,
12
+ };
@@ -1,5 +1,5 @@
1
1
  import { ref, computed, onMounted, onUnmounted } from 'vue';
2
- import { fetchChanges, WidgetDataFetcher } from '@rlse/widget-core';
2
+ import { fetchChanges, WidgetDataFetcher, } from '@rlse/widget-core';
3
3
  export function useWidgetData(options) {
4
4
  const changes = ref([]);
5
5
  const isLoading = ref(true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rlse/widget",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Multi-framework release notes widget for rlse.dev - React, Vue, Angular, Svelte",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -87,7 +87,5 @@
87
87
  "embed"
88
88
  ],
89
89
  "license": "MIT",
90
- "dependencies": {
91
- "@rlse/widget-core": "../widget-core"
92
- }
90
+ "dependencies": {}
93
91
  }