@solutiondesk/changelog 1.19.1 → 1.20.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @solutiondesk/changelog
2
2
 
3
- Typed client for the public [SolutionDesk](https://solutiondesk.io) changelog, plus React hooks and unstyled components. Your team writes the updates in SolutionDesk; your app or website shows them with this package.
3
+ Typed client for the public [SolutionDesk](https://solutiondesk.io) changelogs, plus React hooks and unstyled components. Your team writes the updates for each of its products in SolutionDesk; your app or website shows them with this package.
4
4
 
5
5
  - The client has no dependencies, ships ESM and CommonJS with full TypeScript types, and runs wherever `fetch` does: Node.js 18+, Deno, Bun, edge runtimes and browsers.
6
6
  - No key and no login: the API only ever returns published entries.
@@ -10,39 +10,40 @@ Typed client for the public [SolutionDesk](https://solutiondesk.io) changelog, p
10
10
  npm install @solutiondesk/changelog
11
11
  ```
12
12
 
13
- No code at all? The [Changelog guide](https://help.solutiondesk.io/docs/team/changelog) shows the hosted page and the ready-made "What's new" button (two lines of HTML).
13
+ No code at all? The [Products guide](https://help.solutiondesk.io/docs/team/changelog) shows the hosted page and the ready-made "What's new" button (two lines of HTML).
14
14
 
15
15
  ## Quick start
16
16
 
17
17
  ```ts
18
18
  import { createChangelogClient } from "@solutiondesk/changelog";
19
19
 
20
- const changelog = createChangelogClient({ workspace: "acme" });
20
+ const changelog = createChangelogClient({ workspace: "acme", product: "acme-mobile" });
21
21
 
22
22
  const { title, entries, nextCursor } = await changelog.list({ limit: 10 });
23
23
  const { entry } = await changelog.getEntry("export-to-csv");
24
24
  ```
25
25
 
26
- `workspace` is your workspace's slug, the `acme` in the address shown under **Changelog → Page & button → For developers** (`https://api.solutiondesk.io/v1/changelog/acme`).
26
+ `workspace` and `product` are the two parts of the address shown under **Products → your product → Page & settings → For developers** (`https://api.solutiondesk.io/v1/changelog/acme/acme-mobile`). Leave `product` out to read the workspace's default product.
27
27
 
28
28
  ## What you can read
29
29
 
30
30
  | Method | API | Returns |
31
31
  | --- | --- | --- |
32
- | `list({ category, limit, cursor })` | `GET /v1/changelog/{workspace}` | The team's title and button settings, the hosted `pageUrl`, one page of entries (newest first) and `nextCursor` (`limit` 1–50, default 20). |
32
+ | `list({ category, limit, cursor })` | `GET /v1/changelog/{workspace}/{product}` | The `product`, its title and button settings, the hosted `pageUrl`, one page of entries (newest first) and `nextCursor` (`limit` 1–50, default 20). |
33
33
  | `allEntries({ category })` | the same, page by page | An async iterator over every published entry, e.g. for a sitemap or an RSS feed. |
34
- | `getEntry(slug)` | `GET /v1/changelog/{workspace}/entries/{slug}` | One entry. |
34
+ | `getEntry(slug)` | `GET /v1/changelog/{workspace}/{product}/entries/{slug}` | One entry and its product. |
35
+ | `listProducts()` | `GET /v1/changelog/{workspace}/products` | Every product (name, slug, description, `pageUrl`), the default first. |
35
36
 
36
37
  Each entry has `title`, `summary` (one or two sentences), `category` (`new`, `improved`, `fixed`, `announcement`), `publishedAt`, the details as `markdown` and as `html`, and `url` (the entry on the hosted page). `html` is safe to insert as it is: raw HTML in the team's text is escaped, links only go to web addresses and paths, images only come from https addresses.
37
38
 
38
- A workspace that doesn't exist, a changelog that is turned off and an unpublished entry all answer `404` (`SolutionDeskError` with `isNotFound`).
39
+ A workspace or product that doesn't exist, a changelog that is turned off and an unpublished entry all answer `404` (`SolutionDeskError` with `isNotFound`).
39
40
 
40
41
  ## "New" badge
41
42
 
42
43
  ```ts
43
44
  import { createSeenTracker } from "@solutiondesk/changelog";
44
45
 
45
- const seen = createSeenTracker("acme"); // the browser's localStorage by default
46
+ const seen = createSeenTracker("acme", undefined, "acme-mobile"); // the browser's localStorage by default
46
47
  const unseen = seen.unseen(entries); // entries newer than the visitor's last look
47
48
  seen.markSeen(entries[0].publishedAt); // when they open your panel
48
49
  ```
@@ -57,10 +58,10 @@ On the very first visit nothing counts as new — remember the newest entry then
57
58
  import { ChangelogFeed, WhatsNew } from "@solutiondesk/changelog/react";
58
59
 
59
60
  // In your app's header: a button with the unseen count that opens the latest entries.
60
- <WhatsNew workspace="acme" classNames={{ button: "btn", badge: "badge", panel: "popover" }} />
61
+ <WhatsNew workspace="acme" product="acme-mobile" classNames={{ button: "btn", badge: "badge", panel: "popover" }} />
61
62
 
62
63
  // A whole changelog page: entries with their details and "Older updates".
63
- <ChangelogFeed workspace="acme" locale="de" entryClassNames={{ title: "text-xl font-semibold" }} />
64
+ <ChangelogFeed workspace="acme" product="acme-mobile" locale="de" entryClassNames={{ title: "text-xl font-semibold" }} />
64
65
  ```
65
66
 
66
67
  The components render plain elements with `className` props and `data-sd-*` attributes (`data-sd-changelog-entry`, `data-category`, `data-sd-whats-new-badge` …) and ship no CSS. Both render nothing (or your `fallback`) when the changelog can't be loaded, so they never break your page.
@@ -73,6 +74,7 @@ import { useChangelog } from "@solutiondesk/changelog/react";
73
74
  function Updates() {
74
75
  const { status, entries, unseen, markAllSeen, hasMore, loadMore } = useChangelog({
75
76
  workspace: "acme",
77
+ product: "acme-mobile",
76
78
  limit: 5,
77
79
  });
78
80
  if (status !== "ready") return null;
package/dist/index.cjs CHANGED
@@ -166,8 +166,10 @@ function required(value, name) {
166
166
  // src/client.ts
167
167
  function createChangelogClient(options) {
168
168
  const workspace = required(options.workspace, "workspace");
169
+ const product = options.product?.trim() || void 0;
169
170
  const http = createHttp(options);
170
- const base = `/changelog/${seg(workspace)}`;
171
+ const root = `/changelog/${seg(workspace)}`;
172
+ const base = product ? `${root}/${seg(product)}` : root;
171
173
  const list = (params = {}, requestOptions) => http.request({
172
174
  method: "GET",
173
175
  path: base,
@@ -187,12 +189,18 @@ function createChangelogClient(options) {
187
189
  }
188
190
  return {
189
191
  workspace,
192
+ product,
190
193
  list,
191
194
  allEntries,
192
195
  getEntry: (slug, requestOptions) => http.request({
193
196
  method: "GET",
194
197
  path: `${base}/entries/${seg(required(slug, "slug"))}`,
195
198
  options: requestOptions
199
+ }),
200
+ listProducts: (requestOptions) => http.request({
201
+ method: "GET",
202
+ path: `${root}/products`,
203
+ options: requestOptions
196
204
  })
197
205
  };
198
206
  }
@@ -206,8 +214,8 @@ function browserStorage() {
206
214
  return null;
207
215
  }
208
216
  }
209
- function createSeenTracker(workspace, storage = browserStorage()) {
210
- const key = `sd-changelog-seen:${workspace.toLowerCase()}`;
217
+ function createSeenTracker(workspace, storage = browserStorage(), product) {
218
+ const key = `sd-changelog-seen:${workspace.toLowerCase()}${product ? `:${product.toLowerCase()}` : ""}`;
211
219
  const lastSeen = () => {
212
220
  try {
213
221
  return storage?.getItem(key) ?? null;
package/dist/index.d.cts CHANGED
@@ -108,20 +108,48 @@ interface PublicChangelogEntry {
108
108
  /** One or two sentences — what a "What's new" panel shows. May be empty. */
109
109
  summary: string;
110
110
  category: ChangelogCategory;
111
+ /** The release this change ships in (`2.3.0`); empty when it isn't tied to one. */
112
+ releaseVersion: string;
113
+ /** Where the release can be downloaded (an app store page, GitHub Releases …), or null. */
114
+ downloadUrl: string | null;
111
115
  /** ISO 8601. The list is sorted by it, newest first. */
112
116
  publishedAt: string;
113
117
  /** The details as the team wrote them (Markdown). */
114
118
  markdown: string;
115
119
  /**
116
120
  * The details as safe HTML: raw HTML in the text is escaped, links are only
117
- * http(s)/mailto/relative, images only from https addresses. Ready for
118
- * `innerHTML` / `dangerouslySetInnerHTML`. Empty when there are no details.
121
+ * http(s)/mailto/relative, images only the ones uploaded to the entry (with
122
+ * `srcset`). Ready for `innerHTML` / `dangerouslySetInnerHTML`. Empty when
123
+ * there are no details.
119
124
  */
120
125
  html: string;
121
126
  /** The entry on the workspace's hosted changelog page. */
122
127
  url: string;
123
128
  }
124
- /** `GET /v1/changelog/{workspace}` — settings and one page of entries. */
129
+ /** A product with its own changelog. */
130
+ interface PublicChangelogProduct {
131
+ name: string;
132
+ slug: string;
133
+ /** One sentence about the product. May be empty. */
134
+ description: string;
135
+ /** The product's hosted changelog page. */
136
+ pageUrl: string;
137
+ /** The default product: addresses without a product show this one. */
138
+ isDefault: boolean;
139
+ }
140
+ /** `GET /v1/changelog/{workspace}/products` — every product, the default first. */
141
+ interface PublicChangelogProductsPayload {
142
+ workspace: {
143
+ name: string;
144
+ slug: string;
145
+ logoUrl: string | null;
146
+ };
147
+ products: PublicChangelogProduct[];
148
+ }
149
+ /**
150
+ * `GET /v1/changelog/{workspace}/{product}` — a product's settings and one
151
+ * page of its entries (`GET /v1/changelog/{workspace}`: the default product).
152
+ */
125
153
  interface PublicChangelogPayload {
126
154
  /** The workspace as the hosted page shows it; `logoUrl` is relative to the API. */
127
155
  workspace: {
@@ -129,6 +157,8 @@ interface PublicChangelogPayload {
129
157
  slug: string;
130
158
  logoUrl: string | null;
131
159
  };
160
+ /** The product these entries belong to. */
161
+ product: PublicChangelogProduct;
132
162
  /** The heading the team chose; empty ⇒ use your own ("What's new"). */
133
163
  title: string;
134
164
  /** The team's settings for the "What's new" button (empty label ⇒ your own). */
@@ -136,20 +166,27 @@ interface PublicChangelogPayload {
136
166
  label: string;
137
167
  position: ChangelogWidgetPosition;
138
168
  };
139
- /** The hosted changelog page. */
169
+ /** The product's hosted changelog page. */
140
170
  pageUrl: string;
141
171
  entries: PublicChangelogEntry[];
142
172
  /** Pass as `cursor` for the next page; null on the last one. */
143
173
  nextCursor: string | null;
144
174
  }
145
- /** `GET /v1/changelog/{workspace}/entries/{slug}`. */
175
+ /** `GET /v1/changelog/{workspace}/{product}/entries/{slug}`. */
146
176
  interface PublicChangelogEntryPayload {
177
+ /** The product whose changelog shows the entry. */
178
+ product: PublicChangelogProduct;
147
179
  entry: PublicChangelogEntry;
148
180
  }
149
181
 
150
182
  interface ChangelogClientOptions extends BaseClientOptions {
151
183
  /** Your workspace's slug — the `acme` in `https://api.solutiondesk.io/v1/changelog/acme`. */
152
184
  workspace: string;
185
+ /**
186
+ * The product whose changelog to read (its web address, e.g. `acme-mobile`).
187
+ * Without it, the workspace's default product.
188
+ */
189
+ product?: string;
153
190
  }
154
191
  interface ListEntriesParams {
155
192
  /** Only entries of this category. */
@@ -162,26 +199,32 @@ interface ListEntriesParams {
162
199
  interface ChangelogClient {
163
200
  /** The workspace this client reads. */
164
201
  readonly workspace: string;
202
+ /** The product this client reads, or undefined for the default product. */
203
+ readonly product: string | undefined;
165
204
  /**
166
- * The team's settings and one page of published entries, newest first
167
- * (`GET /v1/changelog/{workspace}`). Throws a `SolutionDeskError` with status
168
- * 404 when the workspace doesn't exist or its changelog is turned off.
205
+ * The product's settings and one page of its published entries, newest first
206
+ * (`GET /v1/changelog/{workspace}/{product}`). Throws a `SolutionDeskError`
207
+ * with status 404 when the workspace or product doesn't exist or the
208
+ * changelog is turned off.
169
209
  */
170
210
  list(params?: ListEntriesParams, options?: RequestOptions): Promise<PublicChangelogPayload>;
171
211
  /** Every published entry, page by page — e.g. for a sitemap, an RSS feed or a static build. */
172
212
  allEntries(params?: Omit<ListEntriesParams, "cursor">, options?: RequestOptions): AsyncGenerator<PublicChangelogEntry, void, undefined>;
173
213
  /**
174
- * One published entry (`GET /v1/changelog/{workspace}/entries/{slug}`).
175
- * Throws a `SolutionDeskError` with status 404 for an unknown or unpublished entry.
214
+ * One published entry of the product (`…/{product}/entries/{slug}`; without
215
+ * a product, of any product). Throws a `SolutionDeskError` with status 404
216
+ * for an unknown or unpublished entry.
176
217
  */
177
218
  getEntry(slug: string, options?: RequestOptions): Promise<PublicChangelogEntryPayload>;
219
+ /** Every product of the workspace, the default first (`GET /v1/changelog/{workspace}/products`). */
220
+ listProducts(options?: RequestOptions): Promise<PublicChangelogProductsPayload>;
178
221
  }
179
222
  /**
180
223
  * A client for the public changelog API — no key, no login; only published
181
224
  * entries. Safe in the browser and on the server.
182
225
  *
183
226
  * @example
184
- * const changelog = createChangelogClient({ workspace: "acme" });
227
+ * const changelog = createChangelogClient({ workspace: "acme", product: "acme-mobile" });
185
228
  * const { entries, nextCursor } = await changelog.list({ limit: 10 });
186
229
  */
187
230
  declare function createChangelogClient(options: ChangelogClientOptions): ChangelogClient;
@@ -213,11 +256,12 @@ interface SeenTracker {
213
256
  /** The browser's `localStorage`, or null (server rendering, privacy mode). */
214
257
  declare function browserStorage(): SeenStorage | null;
215
258
  /**
216
- * A {@link SeenTracker} for one workspace. Uses the key
217
- * `sd-changelog-seen:<workspace>` — the same one the SolutionDesk widget uses,
218
- * so a visitor who opened either sees no badge in the other.
259
+ * A {@link SeenTracker} for one workspace's product. Uses the key
260
+ * `sd-changelog-seen:<workspace>` (default product) or
261
+ * `sd-changelog-seen:<workspace>:<product>` — the same ones the SolutionDesk
262
+ * widget uses, so a visitor who opened either sees no badge in the other.
219
263
  */
220
- declare function createSeenTracker(workspace: string, storage?: SeenStorage | null): SeenTracker;
264
+ declare function createSeenTracker(workspace: string, storage?: SeenStorage | null, product?: string): SeenTracker;
221
265
  type ChangelogLocale = "de" | "en";
222
266
  /** The category names the hosted page uses, for your own UI. */
223
267
  declare const CATEGORY_LABELS: Record<ChangelogLocale, Record<ChangelogCategory, string>>;
@@ -226,4 +270,4 @@ declare const CATEGORY_COLORS: Record<ChangelogCategory, string>;
226
270
  /** A category's name in German (`de*`) or English (everything else). */
227
271
  declare function categoryLabel(category: ChangelogCategory, locale?: string): string;
228
272
 
229
- export { type BaseClientOptions, CATEGORY_COLORS, CATEGORY_LABELS, type CachedResponse, type ChangelogCategory, type ChangelogClient, type ChangelogClientOptions, type ChangelogLocale, type ChangelogWidgetPosition, DEFAULT_BASE_URL, type EtagCache, type FetchInitLike, type FetchLike, type FetchResponseLike, type ListEntriesParams, type PublicChangelogEntry, type PublicChangelogEntryPayload, type PublicChangelogPayload, type RequestOptions, type SeenStorage, type SeenTracker, SolutionDeskError, browserStorage, categoryLabel, createChangelogClient, createMemoryEtagCache, createSeenTracker };
273
+ export { type BaseClientOptions, CATEGORY_COLORS, CATEGORY_LABELS, type CachedResponse, type ChangelogCategory, type ChangelogClient, type ChangelogClientOptions, type ChangelogLocale, type ChangelogWidgetPosition, DEFAULT_BASE_URL, type EtagCache, type FetchInitLike, type FetchLike, type FetchResponseLike, type ListEntriesParams, type PublicChangelogEntry, type PublicChangelogEntryPayload, type PublicChangelogPayload, type PublicChangelogProduct, type PublicChangelogProductsPayload, type RequestOptions, type SeenStorage, type SeenTracker, SolutionDeskError, browserStorage, categoryLabel, createChangelogClient, createMemoryEtagCache, createSeenTracker };
package/dist/index.d.ts CHANGED
@@ -108,20 +108,48 @@ interface PublicChangelogEntry {
108
108
  /** One or two sentences — what a "What's new" panel shows. May be empty. */
109
109
  summary: string;
110
110
  category: ChangelogCategory;
111
+ /** The release this change ships in (`2.3.0`); empty when it isn't tied to one. */
112
+ releaseVersion: string;
113
+ /** Where the release can be downloaded (an app store page, GitHub Releases …), or null. */
114
+ downloadUrl: string | null;
111
115
  /** ISO 8601. The list is sorted by it, newest first. */
112
116
  publishedAt: string;
113
117
  /** The details as the team wrote them (Markdown). */
114
118
  markdown: string;
115
119
  /**
116
120
  * The details as safe HTML: raw HTML in the text is escaped, links are only
117
- * http(s)/mailto/relative, images only from https addresses. Ready for
118
- * `innerHTML` / `dangerouslySetInnerHTML`. Empty when there are no details.
121
+ * http(s)/mailto/relative, images only the ones uploaded to the entry (with
122
+ * `srcset`). Ready for `innerHTML` / `dangerouslySetInnerHTML`. Empty when
123
+ * there are no details.
119
124
  */
120
125
  html: string;
121
126
  /** The entry on the workspace's hosted changelog page. */
122
127
  url: string;
123
128
  }
124
- /** `GET /v1/changelog/{workspace}` — settings and one page of entries. */
129
+ /** A product with its own changelog. */
130
+ interface PublicChangelogProduct {
131
+ name: string;
132
+ slug: string;
133
+ /** One sentence about the product. May be empty. */
134
+ description: string;
135
+ /** The product's hosted changelog page. */
136
+ pageUrl: string;
137
+ /** The default product: addresses without a product show this one. */
138
+ isDefault: boolean;
139
+ }
140
+ /** `GET /v1/changelog/{workspace}/products` — every product, the default first. */
141
+ interface PublicChangelogProductsPayload {
142
+ workspace: {
143
+ name: string;
144
+ slug: string;
145
+ logoUrl: string | null;
146
+ };
147
+ products: PublicChangelogProduct[];
148
+ }
149
+ /**
150
+ * `GET /v1/changelog/{workspace}/{product}` — a product's settings and one
151
+ * page of its entries (`GET /v1/changelog/{workspace}`: the default product).
152
+ */
125
153
  interface PublicChangelogPayload {
126
154
  /** The workspace as the hosted page shows it; `logoUrl` is relative to the API. */
127
155
  workspace: {
@@ -129,6 +157,8 @@ interface PublicChangelogPayload {
129
157
  slug: string;
130
158
  logoUrl: string | null;
131
159
  };
160
+ /** The product these entries belong to. */
161
+ product: PublicChangelogProduct;
132
162
  /** The heading the team chose; empty ⇒ use your own ("What's new"). */
133
163
  title: string;
134
164
  /** The team's settings for the "What's new" button (empty label ⇒ your own). */
@@ -136,20 +166,27 @@ interface PublicChangelogPayload {
136
166
  label: string;
137
167
  position: ChangelogWidgetPosition;
138
168
  };
139
- /** The hosted changelog page. */
169
+ /** The product's hosted changelog page. */
140
170
  pageUrl: string;
141
171
  entries: PublicChangelogEntry[];
142
172
  /** Pass as `cursor` for the next page; null on the last one. */
143
173
  nextCursor: string | null;
144
174
  }
145
- /** `GET /v1/changelog/{workspace}/entries/{slug}`. */
175
+ /** `GET /v1/changelog/{workspace}/{product}/entries/{slug}`. */
146
176
  interface PublicChangelogEntryPayload {
177
+ /** The product whose changelog shows the entry. */
178
+ product: PublicChangelogProduct;
147
179
  entry: PublicChangelogEntry;
148
180
  }
149
181
 
150
182
  interface ChangelogClientOptions extends BaseClientOptions {
151
183
  /** Your workspace's slug — the `acme` in `https://api.solutiondesk.io/v1/changelog/acme`. */
152
184
  workspace: string;
185
+ /**
186
+ * The product whose changelog to read (its web address, e.g. `acme-mobile`).
187
+ * Without it, the workspace's default product.
188
+ */
189
+ product?: string;
153
190
  }
154
191
  interface ListEntriesParams {
155
192
  /** Only entries of this category. */
@@ -162,26 +199,32 @@ interface ListEntriesParams {
162
199
  interface ChangelogClient {
163
200
  /** The workspace this client reads. */
164
201
  readonly workspace: string;
202
+ /** The product this client reads, or undefined for the default product. */
203
+ readonly product: string | undefined;
165
204
  /**
166
- * The team's settings and one page of published entries, newest first
167
- * (`GET /v1/changelog/{workspace}`). Throws a `SolutionDeskError` with status
168
- * 404 when the workspace doesn't exist or its changelog is turned off.
205
+ * The product's settings and one page of its published entries, newest first
206
+ * (`GET /v1/changelog/{workspace}/{product}`). Throws a `SolutionDeskError`
207
+ * with status 404 when the workspace or product doesn't exist or the
208
+ * changelog is turned off.
169
209
  */
170
210
  list(params?: ListEntriesParams, options?: RequestOptions): Promise<PublicChangelogPayload>;
171
211
  /** Every published entry, page by page — e.g. for a sitemap, an RSS feed or a static build. */
172
212
  allEntries(params?: Omit<ListEntriesParams, "cursor">, options?: RequestOptions): AsyncGenerator<PublicChangelogEntry, void, undefined>;
173
213
  /**
174
- * One published entry (`GET /v1/changelog/{workspace}/entries/{slug}`).
175
- * Throws a `SolutionDeskError` with status 404 for an unknown or unpublished entry.
214
+ * One published entry of the product (`…/{product}/entries/{slug}`; without
215
+ * a product, of any product). Throws a `SolutionDeskError` with status 404
216
+ * for an unknown or unpublished entry.
176
217
  */
177
218
  getEntry(slug: string, options?: RequestOptions): Promise<PublicChangelogEntryPayload>;
219
+ /** Every product of the workspace, the default first (`GET /v1/changelog/{workspace}/products`). */
220
+ listProducts(options?: RequestOptions): Promise<PublicChangelogProductsPayload>;
178
221
  }
179
222
  /**
180
223
  * A client for the public changelog API — no key, no login; only published
181
224
  * entries. Safe in the browser and on the server.
182
225
  *
183
226
  * @example
184
- * const changelog = createChangelogClient({ workspace: "acme" });
227
+ * const changelog = createChangelogClient({ workspace: "acme", product: "acme-mobile" });
185
228
  * const { entries, nextCursor } = await changelog.list({ limit: 10 });
186
229
  */
187
230
  declare function createChangelogClient(options: ChangelogClientOptions): ChangelogClient;
@@ -213,11 +256,12 @@ interface SeenTracker {
213
256
  /** The browser's `localStorage`, or null (server rendering, privacy mode). */
214
257
  declare function browserStorage(): SeenStorage | null;
215
258
  /**
216
- * A {@link SeenTracker} for one workspace. Uses the key
217
- * `sd-changelog-seen:<workspace>` — the same one the SolutionDesk widget uses,
218
- * so a visitor who opened either sees no badge in the other.
259
+ * A {@link SeenTracker} for one workspace's product. Uses the key
260
+ * `sd-changelog-seen:<workspace>` (default product) or
261
+ * `sd-changelog-seen:<workspace>:<product>` — the same ones the SolutionDesk
262
+ * widget uses, so a visitor who opened either sees no badge in the other.
219
263
  */
220
- declare function createSeenTracker(workspace: string, storage?: SeenStorage | null): SeenTracker;
264
+ declare function createSeenTracker(workspace: string, storage?: SeenStorage | null, product?: string): SeenTracker;
221
265
  type ChangelogLocale = "de" | "en";
222
266
  /** The category names the hosted page uses, for your own UI. */
223
267
  declare const CATEGORY_LABELS: Record<ChangelogLocale, Record<ChangelogCategory, string>>;
@@ -226,4 +270,4 @@ declare const CATEGORY_COLORS: Record<ChangelogCategory, string>;
226
270
  /** A category's name in German (`de*`) or English (everything else). */
227
271
  declare function categoryLabel(category: ChangelogCategory, locale?: string): string;
228
272
 
229
- export { type BaseClientOptions, CATEGORY_COLORS, CATEGORY_LABELS, type CachedResponse, type ChangelogCategory, type ChangelogClient, type ChangelogClientOptions, type ChangelogLocale, type ChangelogWidgetPosition, DEFAULT_BASE_URL, type EtagCache, type FetchInitLike, type FetchLike, type FetchResponseLike, type ListEntriesParams, type PublicChangelogEntry, type PublicChangelogEntryPayload, type PublicChangelogPayload, type RequestOptions, type SeenStorage, type SeenTracker, SolutionDeskError, browserStorage, categoryLabel, createChangelogClient, createMemoryEtagCache, createSeenTracker };
273
+ export { type BaseClientOptions, CATEGORY_COLORS, CATEGORY_LABELS, type CachedResponse, type ChangelogCategory, type ChangelogClient, type ChangelogClientOptions, type ChangelogLocale, type ChangelogWidgetPosition, DEFAULT_BASE_URL, type EtagCache, type FetchInitLike, type FetchLike, type FetchResponseLike, type ListEntriesParams, type PublicChangelogEntry, type PublicChangelogEntryPayload, type PublicChangelogPayload, type PublicChangelogProduct, type PublicChangelogProductsPayload, type RequestOptions, type SeenStorage, type SeenTracker, SolutionDeskError, browserStorage, categoryLabel, createChangelogClient, createMemoryEtagCache, createSeenTracker };
package/dist/index.js CHANGED
@@ -164,8 +164,10 @@ function required(value, name) {
164
164
  // src/client.ts
165
165
  function createChangelogClient(options) {
166
166
  const workspace = required(options.workspace, "workspace");
167
+ const product = options.product?.trim() || void 0;
167
168
  const http = createHttp(options);
168
- const base = `/changelog/${seg(workspace)}`;
169
+ const root = `/changelog/${seg(workspace)}`;
170
+ const base = product ? `${root}/${seg(product)}` : root;
169
171
  const list = (params = {}, requestOptions) => http.request({
170
172
  method: "GET",
171
173
  path: base,
@@ -185,12 +187,18 @@ function createChangelogClient(options) {
185
187
  }
186
188
  return {
187
189
  workspace,
190
+ product,
188
191
  list,
189
192
  allEntries,
190
193
  getEntry: (slug, requestOptions) => http.request({
191
194
  method: "GET",
192
195
  path: `${base}/entries/${seg(required(slug, "slug"))}`,
193
196
  options: requestOptions
197
+ }),
198
+ listProducts: (requestOptions) => http.request({
199
+ method: "GET",
200
+ path: `${root}/products`,
201
+ options: requestOptions
194
202
  })
195
203
  };
196
204
  }
@@ -204,8 +212,8 @@ function browserStorage() {
204
212
  return null;
205
213
  }
206
214
  }
207
- function createSeenTracker(workspace, storage = browserStorage()) {
208
- const key = `sd-changelog-seen:${workspace.toLowerCase()}`;
215
+ function createSeenTracker(workspace, storage = browserStorage(), product) {
216
+ const key = `sd-changelog-seen:${workspace.toLowerCase()}${product ? `:${product.toLowerCase()}` : ""}`;
209
217
  const lastSeen = () => {
210
218
  try {
211
219
  return storage?.getItem(key) ?? null;
package/dist/react.cjs CHANGED
@@ -16,8 +16,8 @@ function browserStorage() {
16
16
  return null;
17
17
  }
18
18
  }
19
- function createSeenTracker(workspace, storage = browserStorage()) {
20
- const key = `sd-changelog-seen:${workspace.toLowerCase()}`;
19
+ function createSeenTracker(workspace, storage = browserStorage(), product) {
20
+ const key = `sd-changelog-seen:${workspace.toLowerCase()}${product ? `:${product.toLowerCase()}` : ""}`;
21
21
  const lastSeen = () => {
22
22
  try {
23
23
  return storage?.getItem(key) ?? null;
@@ -210,8 +210,10 @@ function required(value, name) {
210
210
  // src/client.ts
211
211
  function createChangelogClient(options) {
212
212
  const workspace = required(options.workspace, "workspace");
213
+ const product = options.product?.trim() || void 0;
213
214
  const http = createHttp(options);
214
- const base = `/changelog/${seg(workspace)}`;
215
+ const root = `/changelog/${seg(workspace)}`;
216
+ const base = product ? `${root}/${seg(product)}` : root;
215
217
  const list = (params = {}, requestOptions) => http.request({
216
218
  method: "GET",
217
219
  path: base,
@@ -231,34 +233,49 @@ function createChangelogClient(options) {
231
233
  }
232
234
  return {
233
235
  workspace,
236
+ product,
234
237
  list,
235
238
  allEntries,
236
239
  getEntry: (slug, requestOptions) => http.request({
237
240
  method: "GET",
238
241
  path: `${base}/entries/${seg(required(slug, "slug"))}`,
239
242
  options: requestOptions
243
+ }),
244
+ listProducts: (requestOptions) => http.request({
245
+ method: "GET",
246
+ path: `${root}/products`,
247
+ options: requestOptions
240
248
  })
241
249
  };
242
250
  }
243
251
 
244
252
  // src/react/use-changelog.ts
245
253
  function useChangelog(options) {
246
- const { workspace, client: given, limit = 10, category, trackSeen = true, initialData } = options;
254
+ const {
255
+ workspace,
256
+ product,
257
+ client: given,
258
+ limit = 10,
259
+ category,
260
+ trackSeen = true,
261
+ initialData
262
+ } = options;
247
263
  const { baseUrl, fetch: fetchImpl, headers, etagCache } = options;
248
264
  const headersKey = JSON.stringify(headers ?? {});
249
265
  const client = react.useMemo(
250
266
  () => given ?? createChangelogClient({
251
267
  workspace,
268
+ product,
252
269
  baseUrl,
253
270
  fetch: fetchImpl,
254
271
  headers: JSON.parse(headersKey),
255
272
  etagCache
256
273
  }),
257
- [given, workspace, baseUrl, fetchImpl, headersKey, etagCache]
274
+ [given, workspace, product, baseUrl, fetchImpl, headersKey, etagCache]
258
275
  );
259
276
  const tracker = react.useMemo(
260
- () => trackSeen ? createSeenTracker(workspace, options.storage) : null,
261
- [trackSeen, workspace, options.storage]
277
+ () => trackSeen ? createSeenTracker(workspace, options.storage, given?.product ?? product) : null,
278
+ [trackSeen, workspace, options.storage, given, product]
262
279
  );
263
280
  const [data, setData] = react.useState(initialData ?? null);
264
281
  const [entries, setEntries] = react.useState(initialData?.entries ?? []);
@@ -364,11 +381,25 @@ function ChangelogEntryView({
364
381
  return /* @__PURE__ */ jsxRuntime.jsxs("article", { "data-sd-changelog-entry": "", "data-category": entry.category, className: classNames.entry, children: [
365
382
  /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-sd-changelog-meta": "", className: classNames.meta, children: [
366
383
  /* @__PURE__ */ jsxRuntime.jsx("time", { dateTime: entry.publishedAt, className: classNames.date, children: formatChangelogDate(entry.publishedAt, locale) }),
367
- /* @__PURE__ */ jsxRuntime.jsx("span", { "data-sd-changelog-category": entry.category, className: classNames.category, children: categoryLabel(entry.category, locale) })
384
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "data-sd-changelog-category": entry.category, className: classNames.category, children: categoryLabel(entry.category, locale) }),
385
+ entry.releaseVersion ? /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-sd-changelog-version": "", className: classNames.version, children: [
386
+ "Version ",
387
+ entry.releaseVersion
388
+ ] }) : null
368
389
  ] }),
369
390
  /* @__PURE__ */ jsxRuntime.jsx(Title, { className: classNames.title, children: /* @__PURE__ */ jsxRuntime.jsx("a", { href: entry.url, children: entry.title }) }),
370
391
  entry.summary ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: classNames.summary, children: entry.summary }) : null,
371
- showBody ? /* @__PURE__ */ jsxRuntime.jsx(ChangelogEntryBody, { html: entry.html, className: classNames.body }) : null
392
+ showBody ? /* @__PURE__ */ jsxRuntime.jsx(ChangelogEntryBody, { html: entry.html, className: classNames.body }) : null,
393
+ entry.downloadUrl ? /* @__PURE__ */ jsxRuntime.jsx(
394
+ "a",
395
+ {
396
+ href: entry.downloadUrl,
397
+ rel: "noopener",
398
+ "data-sd-changelog-download": "",
399
+ className: classNames.download,
400
+ children: locale.toLowerCase().startsWith("de") ? "Herunterladen" : "Download"
401
+ }
402
+ ) : null
372
403
  ] });
373
404
  }
374
405
  function ChangelogFeed(props) {
package/dist/react.d.cts CHANGED
@@ -15,20 +15,48 @@ interface PublicChangelogEntry {
15
15
  /** One or two sentences — what a "What's new" panel shows. May be empty. */
16
16
  summary: string;
17
17
  category: ChangelogCategory;
18
+ /** The release this change ships in (`2.3.0`); empty when it isn't tied to one. */
19
+ releaseVersion: string;
20
+ /** Where the release can be downloaded (an app store page, GitHub Releases …), or null. */
21
+ downloadUrl: string | null;
18
22
  /** ISO 8601. The list is sorted by it, newest first. */
19
23
  publishedAt: string;
20
24
  /** The details as the team wrote them (Markdown). */
21
25
  markdown: string;
22
26
  /**
23
27
  * The details as safe HTML: raw HTML in the text is escaped, links are only
24
- * http(s)/mailto/relative, images only from https addresses. Ready for
25
- * `innerHTML` / `dangerouslySetInnerHTML`. Empty when there are no details.
28
+ * http(s)/mailto/relative, images only the ones uploaded to the entry (with
29
+ * `srcset`). Ready for `innerHTML` / `dangerouslySetInnerHTML`. Empty when
30
+ * there are no details.
26
31
  */
27
32
  html: string;
28
33
  /** The entry on the workspace's hosted changelog page. */
29
34
  url: string;
30
35
  }
31
- /** `GET /v1/changelog/{workspace}` — settings and one page of entries. */
36
+ /** A product with its own changelog. */
37
+ interface PublicChangelogProduct {
38
+ name: string;
39
+ slug: string;
40
+ /** One sentence about the product. May be empty. */
41
+ description: string;
42
+ /** The product's hosted changelog page. */
43
+ pageUrl: string;
44
+ /** The default product: addresses without a product show this one. */
45
+ isDefault: boolean;
46
+ }
47
+ /** `GET /v1/changelog/{workspace}/products` — every product, the default first. */
48
+ interface PublicChangelogProductsPayload {
49
+ workspace: {
50
+ name: string;
51
+ slug: string;
52
+ logoUrl: string | null;
53
+ };
54
+ products: PublicChangelogProduct[];
55
+ }
56
+ /**
57
+ * `GET /v1/changelog/{workspace}/{product}` — a product's settings and one
58
+ * page of its entries (`GET /v1/changelog/{workspace}`: the default product).
59
+ */
32
60
  interface PublicChangelogPayload {
33
61
  /** The workspace as the hosted page shows it; `logoUrl` is relative to the API. */
34
62
  workspace: {
@@ -36,6 +64,8 @@ interface PublicChangelogPayload {
36
64
  slug: string;
37
65
  logoUrl: string | null;
38
66
  };
67
+ /** The product these entries belong to. */
68
+ product: PublicChangelogProduct;
39
69
  /** The heading the team chose; empty ⇒ use your own ("What's new"). */
40
70
  title: string;
41
71
  /** The team's settings for the "What's new" button (empty label ⇒ your own). */
@@ -43,14 +73,16 @@ interface PublicChangelogPayload {
43
73
  label: string;
44
74
  position: ChangelogWidgetPosition;
45
75
  };
46
- /** The hosted changelog page. */
76
+ /** The product's hosted changelog page. */
47
77
  pageUrl: string;
48
78
  entries: PublicChangelogEntry[];
49
79
  /** Pass as `cursor` for the next page; null on the last one. */
50
80
  nextCursor: string | null;
51
81
  }
52
- /** `GET /v1/changelog/{workspace}/entries/{slug}`. */
82
+ /** `GET /v1/changelog/{workspace}/{product}/entries/{slug}`. */
53
83
  interface PublicChangelogEntryPayload {
84
+ /** The product whose changelog shows the entry. */
85
+ product: PublicChangelogProduct;
54
86
  entry: PublicChangelogEntry;
55
87
  }
56
88
 
@@ -122,6 +154,11 @@ interface RequestOptions {
122
154
  interface ChangelogClientOptions extends BaseClientOptions {
123
155
  /** Your workspace's slug — the `acme` in `https://api.solutiondesk.io/v1/changelog/acme`. */
124
156
  workspace: string;
157
+ /**
158
+ * The product whose changelog to read (its web address, e.g. `acme-mobile`).
159
+ * Without it, the workspace's default product.
160
+ */
161
+ product?: string;
125
162
  }
126
163
  interface ListEntriesParams {
127
164
  /** Only entries of this category. */
@@ -134,19 +171,25 @@ interface ListEntriesParams {
134
171
  interface ChangelogClient {
135
172
  /** The workspace this client reads. */
136
173
  readonly workspace: string;
174
+ /** The product this client reads, or undefined for the default product. */
175
+ readonly product: string | undefined;
137
176
  /**
138
- * The team's settings and one page of published entries, newest first
139
- * (`GET /v1/changelog/{workspace}`). Throws a `SolutionDeskError` with status
140
- * 404 when the workspace doesn't exist or its changelog is turned off.
177
+ * The product's settings and one page of its published entries, newest first
178
+ * (`GET /v1/changelog/{workspace}/{product}`). Throws a `SolutionDeskError`
179
+ * with status 404 when the workspace or product doesn't exist or the
180
+ * changelog is turned off.
141
181
  */
142
182
  list(params?: ListEntriesParams, options?: RequestOptions): Promise<PublicChangelogPayload>;
143
183
  /** Every published entry, page by page — e.g. for a sitemap, an RSS feed or a static build. */
144
184
  allEntries(params?: Omit<ListEntriesParams, "cursor">, options?: RequestOptions): AsyncGenerator<PublicChangelogEntry, void, undefined>;
145
185
  /**
146
- * One published entry (`GET /v1/changelog/{workspace}/entries/{slug}`).
147
- * Throws a `SolutionDeskError` with status 404 for an unknown or unpublished entry.
186
+ * One published entry of the product (`…/{product}/entries/{slug}`; without
187
+ * a product, of any product). Throws a `SolutionDeskError` with status 404
188
+ * for an unknown or unpublished entry.
148
189
  */
149
190
  getEntry(slug: string, options?: RequestOptions): Promise<PublicChangelogEntryPayload>;
191
+ /** Every product of the workspace, the default first (`GET /v1/changelog/{workspace}/products`). */
192
+ listProducts(options?: RequestOptions): Promise<PublicChangelogProductsPayload>;
150
193
  }
151
194
 
152
195
  /** Where "last seen" is kept — `window.localStorage`, `sessionStorage` or your own. */
@@ -158,6 +201,8 @@ interface SeenStorage {
158
201
  interface UseChangelogOptions extends Partial<Omit<ChangelogClientOptions, "workspace">> {
159
202
  /** Your workspace's slug. */
160
203
  workspace: string;
204
+ /** The product whose changelog to show (its web address); else the default product. */
205
+ product?: string;
161
206
  /** Use this client instead of making one (e.g. one shared by your whole app). */
162
207
  client?: ChangelogClient;
163
208
  /** Entries per page (1–50, default 10). */
@@ -222,11 +267,18 @@ interface ChangelogEntryClassNames {
222
267
  meta?: string;
223
268
  date?: string;
224
269
  category?: string;
270
+ /** The release (`Version 2.3.0`). */
271
+ version?: string;
272
+ /** The download link. */
273
+ download?: string;
225
274
  title?: string;
226
275
  summary?: string;
227
276
  body?: string;
228
277
  }
229
- /** One entry: date, category, title (linking to the hosted page), summary and details. */
278
+ /**
279
+ * One entry: date, category, release, title (linking to the hosted page),
280
+ * summary, details and the download link.
281
+ */
230
282
  declare function ChangelogEntryView({ entry, locale, showBody, classNames, titleAs: Title, }: {
231
283
  entry: PublicChangelogEntry;
232
284
  /** Language of the date and the category name (`de*` → German, else English). */
package/dist/react.d.ts CHANGED
@@ -15,20 +15,48 @@ interface PublicChangelogEntry {
15
15
  /** One or two sentences — what a "What's new" panel shows. May be empty. */
16
16
  summary: string;
17
17
  category: ChangelogCategory;
18
+ /** The release this change ships in (`2.3.0`); empty when it isn't tied to one. */
19
+ releaseVersion: string;
20
+ /** Where the release can be downloaded (an app store page, GitHub Releases …), or null. */
21
+ downloadUrl: string | null;
18
22
  /** ISO 8601. The list is sorted by it, newest first. */
19
23
  publishedAt: string;
20
24
  /** The details as the team wrote them (Markdown). */
21
25
  markdown: string;
22
26
  /**
23
27
  * The details as safe HTML: raw HTML in the text is escaped, links are only
24
- * http(s)/mailto/relative, images only from https addresses. Ready for
25
- * `innerHTML` / `dangerouslySetInnerHTML`. Empty when there are no details.
28
+ * http(s)/mailto/relative, images only the ones uploaded to the entry (with
29
+ * `srcset`). Ready for `innerHTML` / `dangerouslySetInnerHTML`. Empty when
30
+ * there are no details.
26
31
  */
27
32
  html: string;
28
33
  /** The entry on the workspace's hosted changelog page. */
29
34
  url: string;
30
35
  }
31
- /** `GET /v1/changelog/{workspace}` — settings and one page of entries. */
36
+ /** A product with its own changelog. */
37
+ interface PublicChangelogProduct {
38
+ name: string;
39
+ slug: string;
40
+ /** One sentence about the product. May be empty. */
41
+ description: string;
42
+ /** The product's hosted changelog page. */
43
+ pageUrl: string;
44
+ /** The default product: addresses without a product show this one. */
45
+ isDefault: boolean;
46
+ }
47
+ /** `GET /v1/changelog/{workspace}/products` — every product, the default first. */
48
+ interface PublicChangelogProductsPayload {
49
+ workspace: {
50
+ name: string;
51
+ slug: string;
52
+ logoUrl: string | null;
53
+ };
54
+ products: PublicChangelogProduct[];
55
+ }
56
+ /**
57
+ * `GET /v1/changelog/{workspace}/{product}` — a product's settings and one
58
+ * page of its entries (`GET /v1/changelog/{workspace}`: the default product).
59
+ */
32
60
  interface PublicChangelogPayload {
33
61
  /** The workspace as the hosted page shows it; `logoUrl` is relative to the API. */
34
62
  workspace: {
@@ -36,6 +64,8 @@ interface PublicChangelogPayload {
36
64
  slug: string;
37
65
  logoUrl: string | null;
38
66
  };
67
+ /** The product these entries belong to. */
68
+ product: PublicChangelogProduct;
39
69
  /** The heading the team chose; empty ⇒ use your own ("What's new"). */
40
70
  title: string;
41
71
  /** The team's settings for the "What's new" button (empty label ⇒ your own). */
@@ -43,14 +73,16 @@ interface PublicChangelogPayload {
43
73
  label: string;
44
74
  position: ChangelogWidgetPosition;
45
75
  };
46
- /** The hosted changelog page. */
76
+ /** The product's hosted changelog page. */
47
77
  pageUrl: string;
48
78
  entries: PublicChangelogEntry[];
49
79
  /** Pass as `cursor` for the next page; null on the last one. */
50
80
  nextCursor: string | null;
51
81
  }
52
- /** `GET /v1/changelog/{workspace}/entries/{slug}`. */
82
+ /** `GET /v1/changelog/{workspace}/{product}/entries/{slug}`. */
53
83
  interface PublicChangelogEntryPayload {
84
+ /** The product whose changelog shows the entry. */
85
+ product: PublicChangelogProduct;
54
86
  entry: PublicChangelogEntry;
55
87
  }
56
88
 
@@ -122,6 +154,11 @@ interface RequestOptions {
122
154
  interface ChangelogClientOptions extends BaseClientOptions {
123
155
  /** Your workspace's slug — the `acme` in `https://api.solutiondesk.io/v1/changelog/acme`. */
124
156
  workspace: string;
157
+ /**
158
+ * The product whose changelog to read (its web address, e.g. `acme-mobile`).
159
+ * Without it, the workspace's default product.
160
+ */
161
+ product?: string;
125
162
  }
126
163
  interface ListEntriesParams {
127
164
  /** Only entries of this category. */
@@ -134,19 +171,25 @@ interface ListEntriesParams {
134
171
  interface ChangelogClient {
135
172
  /** The workspace this client reads. */
136
173
  readonly workspace: string;
174
+ /** The product this client reads, or undefined for the default product. */
175
+ readonly product: string | undefined;
137
176
  /**
138
- * The team's settings and one page of published entries, newest first
139
- * (`GET /v1/changelog/{workspace}`). Throws a `SolutionDeskError` with status
140
- * 404 when the workspace doesn't exist or its changelog is turned off.
177
+ * The product's settings and one page of its published entries, newest first
178
+ * (`GET /v1/changelog/{workspace}/{product}`). Throws a `SolutionDeskError`
179
+ * with status 404 when the workspace or product doesn't exist or the
180
+ * changelog is turned off.
141
181
  */
142
182
  list(params?: ListEntriesParams, options?: RequestOptions): Promise<PublicChangelogPayload>;
143
183
  /** Every published entry, page by page — e.g. for a sitemap, an RSS feed or a static build. */
144
184
  allEntries(params?: Omit<ListEntriesParams, "cursor">, options?: RequestOptions): AsyncGenerator<PublicChangelogEntry, void, undefined>;
145
185
  /**
146
- * One published entry (`GET /v1/changelog/{workspace}/entries/{slug}`).
147
- * Throws a `SolutionDeskError` with status 404 for an unknown or unpublished entry.
186
+ * One published entry of the product (`…/{product}/entries/{slug}`; without
187
+ * a product, of any product). Throws a `SolutionDeskError` with status 404
188
+ * for an unknown or unpublished entry.
148
189
  */
149
190
  getEntry(slug: string, options?: RequestOptions): Promise<PublicChangelogEntryPayload>;
191
+ /** Every product of the workspace, the default first (`GET /v1/changelog/{workspace}/products`). */
192
+ listProducts(options?: RequestOptions): Promise<PublicChangelogProductsPayload>;
150
193
  }
151
194
 
152
195
  /** Where "last seen" is kept — `window.localStorage`, `sessionStorage` or your own. */
@@ -158,6 +201,8 @@ interface SeenStorage {
158
201
  interface UseChangelogOptions extends Partial<Omit<ChangelogClientOptions, "workspace">> {
159
202
  /** Your workspace's slug. */
160
203
  workspace: string;
204
+ /** The product whose changelog to show (its web address); else the default product. */
205
+ product?: string;
161
206
  /** Use this client instead of making one (e.g. one shared by your whole app). */
162
207
  client?: ChangelogClient;
163
208
  /** Entries per page (1–50, default 10). */
@@ -222,11 +267,18 @@ interface ChangelogEntryClassNames {
222
267
  meta?: string;
223
268
  date?: string;
224
269
  category?: string;
270
+ /** The release (`Version 2.3.0`). */
271
+ version?: string;
272
+ /** The download link. */
273
+ download?: string;
225
274
  title?: string;
226
275
  summary?: string;
227
276
  body?: string;
228
277
  }
229
- /** One entry: date, category, title (linking to the hosted page), summary and details. */
278
+ /**
279
+ * One entry: date, category, release, title (linking to the hosted page),
280
+ * summary, details and the download link.
281
+ */
230
282
  declare function ChangelogEntryView({ entry, locale, showBody, classNames, titleAs: Title, }: {
231
283
  entry: PublicChangelogEntry;
232
284
  /** Language of the date and the category name (`de*` → German, else English). */
package/dist/react.js CHANGED
@@ -14,8 +14,8 @@ function browserStorage() {
14
14
  return null;
15
15
  }
16
16
  }
17
- function createSeenTracker(workspace, storage = browserStorage()) {
18
- const key = `sd-changelog-seen:${workspace.toLowerCase()}`;
17
+ function createSeenTracker(workspace, storage = browserStorage(), product) {
18
+ const key = `sd-changelog-seen:${workspace.toLowerCase()}${product ? `:${product.toLowerCase()}` : ""}`;
19
19
  const lastSeen = () => {
20
20
  try {
21
21
  return storage?.getItem(key) ?? null;
@@ -208,8 +208,10 @@ function required(value, name) {
208
208
  // src/client.ts
209
209
  function createChangelogClient(options) {
210
210
  const workspace = required(options.workspace, "workspace");
211
+ const product = options.product?.trim() || void 0;
211
212
  const http = createHttp(options);
212
- const base = `/changelog/${seg(workspace)}`;
213
+ const root = `/changelog/${seg(workspace)}`;
214
+ const base = product ? `${root}/${seg(product)}` : root;
213
215
  const list = (params = {}, requestOptions) => http.request({
214
216
  method: "GET",
215
217
  path: base,
@@ -229,34 +231,49 @@ function createChangelogClient(options) {
229
231
  }
230
232
  return {
231
233
  workspace,
234
+ product,
232
235
  list,
233
236
  allEntries,
234
237
  getEntry: (slug, requestOptions) => http.request({
235
238
  method: "GET",
236
239
  path: `${base}/entries/${seg(required(slug, "slug"))}`,
237
240
  options: requestOptions
241
+ }),
242
+ listProducts: (requestOptions) => http.request({
243
+ method: "GET",
244
+ path: `${root}/products`,
245
+ options: requestOptions
238
246
  })
239
247
  };
240
248
  }
241
249
 
242
250
  // src/react/use-changelog.ts
243
251
  function useChangelog(options) {
244
- const { workspace, client: given, limit = 10, category, trackSeen = true, initialData } = options;
252
+ const {
253
+ workspace,
254
+ product,
255
+ client: given,
256
+ limit = 10,
257
+ category,
258
+ trackSeen = true,
259
+ initialData
260
+ } = options;
245
261
  const { baseUrl, fetch: fetchImpl, headers, etagCache } = options;
246
262
  const headersKey = JSON.stringify(headers ?? {});
247
263
  const client = useMemo(
248
264
  () => given ?? createChangelogClient({
249
265
  workspace,
266
+ product,
250
267
  baseUrl,
251
268
  fetch: fetchImpl,
252
269
  headers: JSON.parse(headersKey),
253
270
  etagCache
254
271
  }),
255
- [given, workspace, baseUrl, fetchImpl, headersKey, etagCache]
272
+ [given, workspace, product, baseUrl, fetchImpl, headersKey, etagCache]
256
273
  );
257
274
  const tracker = useMemo(
258
- () => trackSeen ? createSeenTracker(workspace, options.storage) : null,
259
- [trackSeen, workspace, options.storage]
275
+ () => trackSeen ? createSeenTracker(workspace, options.storage, given?.product ?? product) : null,
276
+ [trackSeen, workspace, options.storage, given, product]
260
277
  );
261
278
  const [data, setData] = useState(initialData ?? null);
262
279
  const [entries, setEntries] = useState(initialData?.entries ?? []);
@@ -362,11 +379,25 @@ function ChangelogEntryView({
362
379
  return /* @__PURE__ */ jsxs("article", { "data-sd-changelog-entry": "", "data-category": entry.category, className: classNames.entry, children: [
363
380
  /* @__PURE__ */ jsxs("div", { "data-sd-changelog-meta": "", className: classNames.meta, children: [
364
381
  /* @__PURE__ */ jsx("time", { dateTime: entry.publishedAt, className: classNames.date, children: formatChangelogDate(entry.publishedAt, locale) }),
365
- /* @__PURE__ */ jsx("span", { "data-sd-changelog-category": entry.category, className: classNames.category, children: categoryLabel(entry.category, locale) })
382
+ /* @__PURE__ */ jsx("span", { "data-sd-changelog-category": entry.category, className: classNames.category, children: categoryLabel(entry.category, locale) }),
383
+ entry.releaseVersion ? /* @__PURE__ */ jsxs("span", { "data-sd-changelog-version": "", className: classNames.version, children: [
384
+ "Version ",
385
+ entry.releaseVersion
386
+ ] }) : null
366
387
  ] }),
367
388
  /* @__PURE__ */ jsx(Title, { className: classNames.title, children: /* @__PURE__ */ jsx("a", { href: entry.url, children: entry.title }) }),
368
389
  entry.summary ? /* @__PURE__ */ jsx("p", { className: classNames.summary, children: entry.summary }) : null,
369
- showBody ? /* @__PURE__ */ jsx(ChangelogEntryBody, { html: entry.html, className: classNames.body }) : null
390
+ showBody ? /* @__PURE__ */ jsx(ChangelogEntryBody, { html: entry.html, className: classNames.body }) : null,
391
+ entry.downloadUrl ? /* @__PURE__ */ jsx(
392
+ "a",
393
+ {
394
+ href: entry.downloadUrl,
395
+ rel: "noopener",
396
+ "data-sd-changelog-download": "",
397
+ className: classNames.download,
398
+ children: locale.toLowerCase().startsWith("de") ? "Herunterladen" : "Download"
399
+ }
400
+ ) : null
370
401
  ] });
371
402
  }
372
403
  function ChangelogFeed(props) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solutiondesk/changelog",
3
- "version": "1.19.1",
3
+ "version": "1.20.0",
4
4
  "description": "Typed, zero-dependency client for the SolutionDesk changelog, plus React hooks and unstyled components for a \"What's new\" feed.",
5
5
  "keywords": [
6
6
  "solutiondesk",