@squeletteapp/widget 1.1.0 → 2.0.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.
@@ -0,0 +1,224 @@
1
+ import { createWidget } from '@squeletteapp/widget-builder';
2
+ // ============================================================================
3
+ // Constants
4
+ // ============================================================================
5
+ const CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes
6
+ const STORAGE_KEY = 'squelette_banner_last_ticket';
7
+ const DEFAULT_BASE = 'https://www.squelette.app';
8
+ // ============================================================================
9
+ // Cache Utilities
10
+ // ============================================================================
11
+ function getEnableCache(debug) {
12
+ // Only disable cache when debug is explicitly true
13
+ if (debug === true)
14
+ return false;
15
+ try {
16
+ const userPreference = localStorage.getItem('squelette-enable-cache');
17
+ if (userPreference === 'false')
18
+ return false;
19
+ return true;
20
+ }
21
+ catch {
22
+ return true;
23
+ }
24
+ }
25
+ async function fetchLastChangelogTicket(base, slug, debug) {
26
+ try {
27
+ const enableCache = getEnableCache(debug);
28
+ // Check cache first if enabled
29
+ if (enableCache) {
30
+ const cacheKey = `squelette_banner_cache_${slug}`;
31
+ const cached = localStorage.getItem(cacheKey);
32
+ if (cached) {
33
+ const { ticket, timestamp } = JSON.parse(cached);
34
+ const isExpired = Date.now() - timestamp > CACHE_DURATION_MS;
35
+ if (!isExpired) {
36
+ return ticket;
37
+ }
38
+ }
39
+ }
40
+ // Fetch from API
41
+ const url = `${base}/api/public/w/${slug}/changelog/last`;
42
+ const response = await fetch(url);
43
+ const json = (await response.json());
44
+ const ticket = json?.ticket ?? null;
45
+ // Store in cache if enabled
46
+ if (enableCache && ticket) {
47
+ const cacheKey = `squelette_banner_cache_${slug}`;
48
+ const cachedData = {
49
+ ticket,
50
+ timestamp: Date.now(),
51
+ };
52
+ localStorage.setItem(cacheKey, JSON.stringify(cachedData));
53
+ }
54
+ return ticket;
55
+ }
56
+ catch (e) {
57
+ console.error('[SqueletteBanner] Error fetching changelog ticket:', e);
58
+ return null;
59
+ }
60
+ }
61
+ function shouldShowBanner(ticket, debug) {
62
+ const lastSeenCreatedAt = localStorage.getItem(STORAGE_KEY);
63
+ // Show if cache is disabled, never seen before, or newer ticket
64
+ return (!getEnableCache(debug) ||
65
+ !lastSeenCreatedAt ||
66
+ new Date(ticket.created_at) > new Date(lastSeenCreatedAt));
67
+ }
68
+ function markTicketAsSeen(ticket) {
69
+ localStorage.setItem(STORAGE_KEY, ticket.created_at);
70
+ }
71
+ /**
72
+ * Wrap a Widget with setTheme functionality
73
+ * Sends a THEME_UPDATE message to the iframe instead of reloading
74
+ */
75
+ function withTheme(widget) {
76
+ return {
77
+ ...widget,
78
+ setTheme(theme) {
79
+ const message = {
80
+ type: 'THEME_UPDATE',
81
+ payload: { theme },
82
+ };
83
+ widget.sendMessage(message);
84
+ },
85
+ };
86
+ }
87
+ // ============================================================================
88
+ // Widget Store
89
+ // ============================================================================
90
+ export function createChangelogStore() {
91
+ const widgets = {};
92
+ return {
93
+ getState: () => widgets,
94
+ mount: async (source, widget) => {
95
+ widgets[source] = widget;
96
+ widget.preload();
97
+ },
98
+ open: async (source) => {
99
+ const widget = widgets[source];
100
+ if (widget) {
101
+ widget.open();
102
+ }
103
+ },
104
+ unmount: (source) => {
105
+ const widget = widgets[source];
106
+ if (widget) {
107
+ widget.destroy();
108
+ delete widgets[source];
109
+ }
110
+ },
111
+ setTheme: (theme) => {
112
+ for (const widget of Object.values(widgets)) {
113
+ widget.setTheme(theme);
114
+ }
115
+ },
116
+ };
117
+ }
118
+ // ============================================================================
119
+ // Widget Creators
120
+ // ============================================================================
121
+ export function createChangelogEntryWidget(ticketId, options = {}) {
122
+ const { base = DEFAULT_BASE, contentTheme = 'light', slug = 'squelette' } = options;
123
+ const widget = createWidget({
124
+ elementName: 'sq-changelog-entry-widget',
125
+ source: `${base}/widget/${contentTheme}/${slug}/changelog/${ticketId}`,
126
+ position: 'center',
127
+ overlay: true,
128
+ });
129
+ return withTheme(widget);
130
+ }
131
+ /**
132
+ * Creates and initializes a changelog banner widget.
133
+ * - Fetches the latest changelog ticket
134
+ * - Checks if it should be shown (not already seen)
135
+ * - Preloads the iframe, which will send OPEN_WIDGET when ready
136
+ * - Returns null if nothing to show
137
+ */
138
+ export async function createChangelogBannerWidget(store, options) {
139
+ const { base = DEFAULT_BASE, contentTheme = 'light', slug, debug = false } = options;
140
+ // Fetch the latest ticket
141
+ const ticket = await fetchLastChangelogTicket(base, slug, debug);
142
+ if (!ticket) {
143
+ return null;
144
+ }
145
+ const shouldRender = shouldShowBanner(ticket, debug);
146
+ // Check if we should show the banner
147
+ if (!shouldRender) {
148
+ return null;
149
+ }
150
+ const ticketId = String(ticket.id);
151
+ // Preload the entry widget for when user clicks the banner
152
+ store.mount(ticketId, createChangelogEntryWidget(ticketId, { base, contentTheme, slug }));
153
+ const widget = createWidget({
154
+ elementName: 'sq-changelog-banner-widget',
155
+ source: `${base}/widget/${contentTheme}/${slug}/changelog/${ticketId}/banner`,
156
+ position: 's',
157
+ overlay: false,
158
+ fitToContent: true,
159
+ style: {
160
+ borderRadius: 32,
161
+ height: 32,
162
+ },
163
+ onMessage: (type, message, actions) => {
164
+ // Iframe signals it's ready to be shown
165
+ if (type === 'OPEN_WIDGET') {
166
+ widget.open();
167
+ }
168
+ // User clicked on the banner content
169
+ if (type === 'OPEN_CHANGELOG_ENTRY') {
170
+ const id = message.payload.ticketId;
171
+ actions.close();
172
+ store.open(id);
173
+ }
174
+ },
175
+ });
176
+ // Register callback to mark ticket as seen when banner closes
177
+ widget.onOpenChange((isOpen) => {
178
+ if (!isOpen) {
179
+ markTicketAsSeen(ticket);
180
+ }
181
+ });
182
+ // Preload the iframe - it will send OPEN_WIDGET when content is ready
183
+ widget.preload();
184
+ return withTheme(widget);
185
+ }
186
+ export function createChangelogEntriesListDropdownWidget(anchor, store, options = {}) {
187
+ const { base = DEFAULT_BASE, contentTheme = 'light', slug = 'squelette', position = 'nw' } = options;
188
+ const widget = createWidget({
189
+ elementName: 'sq-changelog-entries-list-dropdown-widget',
190
+ source: `${base}/widget/${contentTheme}/${slug}/changelog/list`,
191
+ position,
192
+ anchor,
193
+ fitToContent: true,
194
+ onMessage: (type, message, actions) => {
195
+ if (type === 'PRELOAD_CHANGELOG_ENTRIES') {
196
+ const ids = message.payload.ids;
197
+ for (const id of ids) {
198
+ store.mount(id, createChangelogEntryWidget(id, { base, contentTheme, slug }));
199
+ }
200
+ }
201
+ if (type === 'OPEN_CHANGELOG_ENTRY') {
202
+ const ticketId = message.payload.ticketId;
203
+ actions.close();
204
+ store.open(ticketId);
205
+ }
206
+ },
207
+ });
208
+ return withTheme(widget);
209
+ }
210
+ // ============================================================================
211
+ // Utility
212
+ // ============================================================================
213
+ /**
214
+ * Clear the cache for a specific workspace
215
+ */
216
+ export function clearBannerCache(slug) {
217
+ try {
218
+ localStorage.removeItem(`squelette_banner_cache_${slug}`);
219
+ localStorage.removeItem(STORAGE_KEY);
220
+ }
221
+ catch {
222
+ // Ignore localStorage errors
223
+ }
224
+ }