@solutiondesk/changelog 0.0.0-stage → 1.0.0-9fc05ac

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wemogy GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,91 @@
1
- # Temporary Holding Version
1
+ # @solutiondesk/changelog
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.
4
+
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
+ - No key and no login: the API only ever returns published entries.
7
+ - `@solutiondesk/changelog/react`: a hook and unstyled components (`ChangelogFeed`, `WhatsNew`) that take your design system's styles. React is an optional peer dependency.
8
+
9
+ ```bash
10
+ npm install @solutiondesk/changelog
11
+ ```
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).
14
+
15
+ ## Quick start
16
+
17
+ ```ts
18
+ import { createChangelogClient } from "@solutiondesk/changelog";
19
+
20
+ const changelog = createChangelogClient({ workspace: "acme" });
21
+
22
+ const { title, entries, nextCursor } = await changelog.list({ limit: 10 });
23
+ const { entry } = await changelog.getEntry("export-to-csv");
24
+ ```
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`).
27
+
28
+ ## What you can read
29
+
30
+ | Method | API | Returns |
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). |
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. |
35
+
36
+ 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
+ A workspace that doesn't exist, a changelog that is turned off and an unpublished entry all answer `404` (`SolutionDeskError` with `isNotFound`).
39
+
40
+ ## "New" badge
41
+
42
+ ```ts
43
+ import { createSeenTracker } from "@solutiondesk/changelog";
44
+
45
+ const seen = createSeenTracker("acme"); // the browser's localStorage by default
46
+ const unseen = seen.unseen(entries); // entries newer than the visitor's last look
47
+ seen.markSeen(entries[0].publishedAt); // when they open your panel
48
+ ```
49
+
50
+ On the very first visit nothing counts as new — remember the newest entry then, so the next one is the first that counts. The tracker uses the same key as SolutionDesk's own button, and nothing about the visitor leaves their browser.
51
+
52
+ `categoryLabel(category, locale)`, `CATEGORY_LABELS` and `CATEGORY_COLORS` give you the names (German and English) and colors the hosted page uses.
53
+
54
+ ## React
55
+
56
+ ```tsx
57
+ import { ChangelogFeed, WhatsNew } from "@solutiondesk/changelog/react";
58
+
59
+ // 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
+
62
+ // A whole changelog page: entries with their details and "Older updates".
63
+ <ChangelogFeed workspace="acme" locale="de" entryClassNames={{ title: "text-xl font-semibold" }} />
64
+ ```
65
+
66
+ 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.
67
+
68
+ For your own markup, use the hook:
69
+
70
+ ```tsx
71
+ import { useChangelog } from "@solutiondesk/changelog/react";
72
+
73
+ function Updates() {
74
+ const { status, entries, unseen, markAllSeen, hasMore, loadMore } = useChangelog({
75
+ workspace: "acme",
76
+ limit: 5,
77
+ });
78
+ if (status !== "ready") return null;
79
+ // …
80
+ }
81
+ ```
82
+
83
+ Pass `initialData` (a `list()` answer fetched on your server) for a first paint without loading. `ChangelogEntryView` and `ChangelogEntryBody` render a single entry or just its details.
84
+
85
+ ## Options
86
+
87
+ `createChangelogClient` (and the hook) take `baseUrl` (default `https://api.solutiondesk.io`), `fetch`, `headers` and `etagCache` (revalidate with `If-None-Match`). Per call, `fetchOptions` is handed to `fetch` unchanged — e.g. Next.js `{ next: { revalidate: 300, tags: ["changelog"] } }`.
88
+
89
+ ## License
90
+
91
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,255 @@
1
+ 'use strict';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
5
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
6
+
7
+ // ../sdk-core/src/http.ts
8
+ var DEFAULT_BASE_URL = "https://api.solutiondesk.io";
9
+ var SolutionDeskError = class extends Error {
10
+ constructor(message, init) {
11
+ super(message);
12
+ /** The HTTP status; `0` when no answer arrived (network error, aborted). */
13
+ __publicField(this, "status");
14
+ /** The parsed JSON body of the error answer (usually `{ error: string }`), else its text. */
15
+ __publicField(this, "body");
16
+ /** The requested URL. */
17
+ __publicField(this, "url");
18
+ /** Messages per input field on a `400` (e.g. `{ email: "Enter a valid email address." }`). */
19
+ __publicField(this, "fields");
20
+ this.name = "SolutionDeskError";
21
+ this.status = init.status;
22
+ this.body = init.body ?? null;
23
+ this.url = init.url;
24
+ this.fields = fieldsOf(init.body);
25
+ if (init.cause !== void 0) {
26
+ Object.defineProperty(this, "cause", { value: init.cause, enumerable: false });
27
+ }
28
+ }
29
+ /** `404`: unknown workspace, module turned off or unknown item — on purpose indistinguishable. */
30
+ get isNotFound() {
31
+ return this.status === 404;
32
+ }
33
+ /** `429`: over the rate limit; try again later. */
34
+ get isRateLimited() {
35
+ return this.status === 429;
36
+ }
37
+ };
38
+ function fieldsOf(body) {
39
+ if (!body || typeof body !== "object") return {};
40
+ const fields = body.fields;
41
+ if (!fields || typeof fields !== "object") return {};
42
+ const out = {};
43
+ for (const [key, value] of Object.entries(fields)) {
44
+ if (typeof value === "string") out[key] = value;
45
+ }
46
+ return out;
47
+ }
48
+ function messageOf(body, status) {
49
+ if (body && typeof body === "object") {
50
+ const { error, message, statusCode } = body;
51
+ if (typeof statusCode === "number" && typeof message === "string" && message) return message;
52
+ if (typeof error === "string" && error) return error;
53
+ if (typeof message === "string" && message) return message;
54
+ if (Array.isArray(message) && typeof message[0] === "string") return message[0];
55
+ }
56
+ if (typeof body === "string" && body.trim() && body.length <= 300) return body.trim();
57
+ return `SolutionDesk answered ${status}.`;
58
+ }
59
+ function createMemoryEtagCache(maxEntries = 500) {
60
+ const entries = /* @__PURE__ */ new Map();
61
+ return {
62
+ get: (key) => entries.get(key),
63
+ set(key, value) {
64
+ entries.delete(key);
65
+ entries.set(key, value);
66
+ if (entries.size > maxEntries) {
67
+ const oldest = entries.keys().next().value;
68
+ if (oldest !== void 0) entries.delete(oldest);
69
+ }
70
+ }
71
+ };
72
+ }
73
+ function queryString(query) {
74
+ if (!query) return "";
75
+ const parts = [];
76
+ for (const [key, value] of Object.entries(query)) {
77
+ if (value === void 0 || value === null || value === "") continue;
78
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
79
+ }
80
+ return parts.length ? `?${parts.join("&")}` : "";
81
+ }
82
+ var seg = (value) => encodeURIComponent(String(value));
83
+ function parseBody(text) {
84
+ if (!text) return null;
85
+ try {
86
+ return JSON.parse(text);
87
+ } catch {
88
+ return text;
89
+ }
90
+ }
91
+ function createHttp(options = {}) {
92
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "").replace(/\/v1$/, "");
93
+ const fetchImpl = options.fetch ?? globalThis.fetch;
94
+ const cache = options.etagCache === true ? createMemoryEtagCache() : options.etagCache ? options.etagCache : void 0;
95
+ const url = (path, query) => `${baseUrl}/v1${path}${queryString(query)}`;
96
+ async function requestWithMeta(req) {
97
+ if (!fetchImpl) {
98
+ throw new Error("No global fetch available: pass `fetch` to the client.");
99
+ }
100
+ const target = url(req.path, req.query);
101
+ const headers = {
102
+ accept: req.accept ?? "application/json",
103
+ ...options.headers,
104
+ ...req.options?.headers
105
+ };
106
+ if (req.body !== void 0) headers["content-type"] = "application/json";
107
+ if (req.apiKey) headers.authorization = `Bearer ${req.apiKey}`;
108
+ const cacheKey = req.method === "GET" && !req.apiKey ? target : void 0;
109
+ const cached = cacheKey && cache ? cache.get(cacheKey) : void 0;
110
+ if (cached && !Object.keys(headers).some((h) => h.toLowerCase() === "if-none-match")) {
111
+ headers["if-none-match"] = cached.etag;
112
+ }
113
+ const init = {
114
+ ...req.options?.fetchOptions,
115
+ method: req.method,
116
+ headers
117
+ };
118
+ if (req.body !== void 0) init.body = JSON.stringify(req.body);
119
+ if (req.options?.signal !== void 0) init.signal = req.options.signal;
120
+ let response;
121
+ try {
122
+ response = await fetchImpl(target, init);
123
+ } catch (cause) {
124
+ const reason = cause instanceof Error ? cause.message : String(cause);
125
+ throw new SolutionDeskError(`The request to SolutionDesk failed: ${reason}`, {
126
+ status: 0,
127
+ url: target,
128
+ cause
129
+ });
130
+ }
131
+ const finalUrl = response.url;
132
+ const meta = {
133
+ status: response.status,
134
+ finalUrl: typeof finalUrl === "string" && finalUrl ? finalUrl : null
135
+ };
136
+ if (response.status === 304 && cached) {
137
+ return { data: cached.body, ...meta };
138
+ }
139
+ const text = await response.text();
140
+ const body = response.ok && req.responseType === "text" ? text : parseBody(text);
141
+ if (!response.ok) {
142
+ throw new SolutionDeskError(messageOf(body, response.status), {
143
+ status: response.status,
144
+ body,
145
+ url: target
146
+ });
147
+ }
148
+ const etag = response.headers.get("etag");
149
+ if (cacheKey && cache && etag) cache.set(cacheKey, { etag, body });
150
+ return { data: body, ...meta };
151
+ }
152
+ return {
153
+ baseUrl,
154
+ url,
155
+ requestWithMeta,
156
+ request: async (req) => (await requestWithMeta(req)).data
157
+ };
158
+ }
159
+ function required(value, name) {
160
+ if (typeof value !== "string" || !value.trim()) {
161
+ throw new TypeError(`SolutionDesk: \`${name}\` is required.`);
162
+ }
163
+ return value;
164
+ }
165
+
166
+ // src/client.ts
167
+ function createChangelogClient(options) {
168
+ const workspace = required(options.workspace, "workspace");
169
+ const http = createHttp(options);
170
+ const base = `/changelog/${seg(workspace)}`;
171
+ const list = (params = {}, requestOptions) => http.request({
172
+ method: "GET",
173
+ path: base,
174
+ query: { category: params.category, limit: params.limit, cursor: params.cursor },
175
+ options: requestOptions
176
+ });
177
+ async function* allEntries(params = {}, requestOptions) {
178
+ let cursor = null;
179
+ do {
180
+ const page = await list(
181
+ { ...params, limit: params.limit ?? 50, cursor },
182
+ requestOptions
183
+ );
184
+ yield* page.entries;
185
+ cursor = page.nextCursor;
186
+ } while (cursor);
187
+ }
188
+ return {
189
+ workspace,
190
+ list,
191
+ allEntries,
192
+ getEntry: (slug, requestOptions) => http.request({
193
+ method: "GET",
194
+ path: `${base}/entries/${seg(required(slug, "slug"))}`,
195
+ options: requestOptions
196
+ })
197
+ };
198
+ }
199
+
200
+ // src/seen.ts
201
+ function browserStorage() {
202
+ try {
203
+ const storage = globalThis.localStorage;
204
+ return storage ?? null;
205
+ } catch {
206
+ return null;
207
+ }
208
+ }
209
+ function createSeenTracker(workspace, storage = browserStorage()) {
210
+ const key = `sd-changelog-seen:${workspace.toLowerCase()}`;
211
+ const lastSeen = () => {
212
+ try {
213
+ return storage?.getItem(key) ?? null;
214
+ } catch {
215
+ return null;
216
+ }
217
+ };
218
+ return {
219
+ lastSeen,
220
+ markSeen(publishedAt) {
221
+ try {
222
+ storage?.setItem(key, publishedAt);
223
+ } catch {
224
+ }
225
+ },
226
+ unseen(entries) {
227
+ const seen = lastSeen();
228
+ if (!seen) return 0;
229
+ return entries.filter((entry) => entry.publishedAt > seen).length;
230
+ }
231
+ };
232
+ }
233
+ var CATEGORY_LABELS = {
234
+ en: { new: "New", improved: "Improved", fixed: "Fixed", announcement: "Announcement" },
235
+ de: { new: "Neu", improved: "Verbessert", fixed: "Behoben", announcement: "Ank\xFCndigung" }
236
+ };
237
+ var CATEGORY_COLORS = {
238
+ new: "#2563eb",
239
+ improved: "#7c3aed",
240
+ fixed: "#16a34a",
241
+ announcement: "#d97706"
242
+ };
243
+ function categoryLabel(category, locale = "en") {
244
+ return CATEGORY_LABELS[locale.toLowerCase().startsWith("de") ? "de" : "en"][category];
245
+ }
246
+
247
+ exports.CATEGORY_COLORS = CATEGORY_COLORS;
248
+ exports.CATEGORY_LABELS = CATEGORY_LABELS;
249
+ exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
250
+ exports.SolutionDeskError = SolutionDeskError;
251
+ exports.browserStorage = browserStorage;
252
+ exports.categoryLabel = categoryLabel;
253
+ exports.createChangelogClient = createChangelogClient;
254
+ exports.createMemoryEtagCache = createMemoryEtagCache;
255
+ exports.createSeenTracker = createSeenTracker;
@@ -0,0 +1,229 @@
1
+ /**
2
+ * The HTTP part shared by the public SolutionDesk SDKs. Runs on any runtime with
3
+ * a WHATWG `fetch` (Node ≥ 18, Deno, Bun, edge runtimes, browsers) — and on
4
+ * purpose names no DOM or Node type, so the published `.d.ts` files need neither
5
+ * `lib: ["dom"]` nor `@types/node`.
6
+ */
7
+ /** The production API. */
8
+ declare const DEFAULT_BASE_URL = "https://api.solutiondesk.io";
9
+ /** The response parts the SDK reads — what every `fetch` implementation returns. */
10
+ interface FetchResponseLike {
11
+ readonly status: number;
12
+ readonly ok: boolean;
13
+ readonly headers: {
14
+ get(name: string): string | null;
15
+ };
16
+ text(): Promise<string>;
17
+ }
18
+ /** What the SDK passes to `fetch` (plus whatever you set in `fetchOptions`). */
19
+ interface FetchInitLike {
20
+ method: string;
21
+ headers: Record<string, string>;
22
+ body?: string;
23
+ }
24
+ /**
25
+ * A `fetch` implementation. The global `fetch` fits, so do `undici`, `node-fetch`
26
+ * and wrappers (e.g. one that adds tracing). Declared bivariantly so a fetch with
27
+ * a richer `init` type is accepted.
28
+ */
29
+ type FetchLike = {
30
+ bivarianceHack(input: string, init?: FetchInitLike): Promise<FetchResponseLike>;
31
+ }["bivarianceHack"];
32
+ /** A response kept for `If-None-Match` revalidation. */
33
+ interface CachedResponse {
34
+ etag: string;
35
+ body: unknown;
36
+ }
37
+ /**
38
+ * Where the client keeps answers for ETag revalidation. Any `Map` fits; bring
39
+ * your own to share it between clients or to bound it differently.
40
+ */
41
+ interface EtagCache {
42
+ get(key: string): CachedResponse | undefined;
43
+ set(key: string, value: CachedResponse): unknown;
44
+ }
45
+ /** Options every client factory takes. */
46
+ interface BaseClientOptions {
47
+ /** The API origin, without `/v1`. Defaults to `https://api.solutiondesk.io`. */
48
+ baseUrl?: string;
49
+ /** A `fetch` implementation; defaults to the global one. */
50
+ fetch?: FetchLike;
51
+ /** Headers sent with every request (e.g. `accept-language` forwarded from your visitor). */
52
+ headers?: Record<string, string>;
53
+ /**
54
+ * Revalidate with `If-None-Match`: the client remembers each answer's `ETag`
55
+ * and, when the API answers `304 Not Modified`, returns the remembered body.
56
+ * `true` keeps up to 500 answers in memory; or pass your own {@link EtagCache}.
57
+ * Off by default (your framework's fetch cache may already do this).
58
+ */
59
+ etagCache?: boolean | EtagCache;
60
+ }
61
+ /** Per-call options. */
62
+ interface RequestOptions {
63
+ /** Cancels the request (an `AbortSignal`). */
64
+ signal?: unknown;
65
+ /** Extra headers for this request. */
66
+ headers?: Record<string, string>;
67
+ /**
68
+ * Extra `fetch` options for this request, passed through unchanged — e.g.
69
+ * Next.js `{ next: { revalidate: 60, tags: ["legal"] } }` or `{ cache: "no-store" }`.
70
+ */
71
+ fetchOptions?: Record<string, unknown>;
72
+ }
73
+ /** Any non-2xx answer of the API (and a failed network request, with `status` 0). */
74
+ declare class SolutionDeskError extends Error {
75
+ /** The HTTP status; `0` when no answer arrived (network error, aborted). */
76
+ readonly status: number;
77
+ /** The parsed JSON body of the error answer (usually `{ error: string }`), else its text. */
78
+ readonly body: unknown;
79
+ /** The requested URL. */
80
+ readonly url: string;
81
+ /** Messages per input field on a `400` (e.g. `{ email: "Enter a valid email address." }`). */
82
+ readonly fields: Record<string, string>;
83
+ constructor(message: string, init: {
84
+ status: number;
85
+ body?: unknown;
86
+ url: string;
87
+ cause?: unknown;
88
+ });
89
+ /** `404`: unknown workspace, module turned off or unknown item — on purpose indistinguishable. */
90
+ get isNotFound(): boolean;
91
+ /** `429`: over the rate limit; try again later. */
92
+ get isRateLimited(): boolean;
93
+ }
94
+ /** A bounded in-memory {@link EtagCache} (oldest entry dropped first). */
95
+ declare function createMemoryEtagCache(maxEntries?: number): EtagCache;
96
+
97
+ /**
98
+ * The answers of the public changelog API (`GET /v1/changelog/{workspace}/…`).
99
+ * Written by hand so the package has no dependencies; a drift test in the
100
+ * SolutionDesk repository keeps them identical to the API's own contracts.
101
+ */
102
+ type ChangelogCategory = "new" | "improved" | "fixed" | "announcement";
103
+ type ChangelogWidgetPosition = "bottom-right" | "bottom-left";
104
+ /** A published entry. */
105
+ interface PublicChangelogEntry {
106
+ slug: string;
107
+ title: string;
108
+ /** One or two sentences — what a "What's new" panel shows. May be empty. */
109
+ summary: string;
110
+ category: ChangelogCategory;
111
+ /** ISO 8601. The list is sorted by it, newest first. */
112
+ publishedAt: string;
113
+ /** The details as the team wrote them (Markdown). */
114
+ markdown: string;
115
+ /**
116
+ * 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.
119
+ */
120
+ html: string;
121
+ /** The entry on the workspace's hosted changelog page. */
122
+ url: string;
123
+ }
124
+ /** `GET /v1/changelog/{workspace}` — settings and one page of entries. */
125
+ interface PublicChangelogPayload {
126
+ /** The workspace as the hosted page shows it; `logoUrl` is relative to the API. */
127
+ workspace: {
128
+ name: string;
129
+ slug: string;
130
+ logoUrl: string | null;
131
+ };
132
+ /** The heading the team chose; empty ⇒ use your own ("What's new"). */
133
+ title: string;
134
+ /** The team's settings for the "What's new" button (empty label ⇒ your own). */
135
+ widget: {
136
+ label: string;
137
+ position: ChangelogWidgetPosition;
138
+ };
139
+ /** The hosted changelog page. */
140
+ pageUrl: string;
141
+ entries: PublicChangelogEntry[];
142
+ /** Pass as `cursor` for the next page; null on the last one. */
143
+ nextCursor: string | null;
144
+ }
145
+ /** `GET /v1/changelog/{workspace}/entries/{slug}`. */
146
+ interface PublicChangelogEntryPayload {
147
+ entry: PublicChangelogEntry;
148
+ }
149
+
150
+ interface ChangelogClientOptions extends BaseClientOptions {
151
+ /** Your workspace's slug — the `acme` in `https://api.solutiondesk.io/v1/changelog/acme`. */
152
+ workspace: string;
153
+ }
154
+ interface ListEntriesParams {
155
+ /** Only entries of this category. */
156
+ category?: ChangelogCategory;
157
+ /** Page size, 1–50 (default 20). */
158
+ limit?: number;
159
+ /** `nextCursor` of the previous page. */
160
+ cursor?: string | null;
161
+ }
162
+ interface ChangelogClient {
163
+ /** The workspace this client reads. */
164
+ readonly workspace: string;
165
+ /**
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.
169
+ */
170
+ list(params?: ListEntriesParams, options?: RequestOptions): Promise<PublicChangelogPayload>;
171
+ /** Every published entry, page by page — e.g. for a sitemap, an RSS feed or a static build. */
172
+ allEntries(params?: Omit<ListEntriesParams, "cursor">, options?: RequestOptions): AsyncGenerator<PublicChangelogEntry, void, undefined>;
173
+ /**
174
+ * One published entry (`GET /v1/changelog/{workspace}/entries/{slug}`).
175
+ * Throws a `SolutionDeskError` with status 404 for an unknown or unpublished entry.
176
+ */
177
+ getEntry(slug: string, options?: RequestOptions): Promise<PublicChangelogEntryPayload>;
178
+ }
179
+ /**
180
+ * A client for the public changelog API — no key, no login; only published
181
+ * entries. Safe in the browser and on the server.
182
+ *
183
+ * @example
184
+ * const changelog = createChangelogClient({ workspace: "acme" });
185
+ * const { entries, nextCursor } = await changelog.list({ limit: 10 });
186
+ */
187
+ declare function createChangelogClient(options: ChangelogClientOptions): ChangelogClient;
188
+
189
+ /** Where "last seen" is kept — `window.localStorage`, `sessionStorage` or your own. */
190
+ interface SeenStorage {
191
+ getItem(key: string): string | null;
192
+ setItem(key: string, value: string): void;
193
+ }
194
+ /**
195
+ * Remembers when a visitor last looked at the changelog, so you can show a
196
+ * "new" badge. It lives where you keep it (by default the browser's
197
+ * `localStorage`); nothing about the visitor is sent to SolutionDesk.
198
+ */
199
+ interface SeenTracker {
200
+ /** ISO time of the newest entry the visitor has seen, or null before the first look. */
201
+ lastSeen(): string | null;
202
+ /** Remember that the visitor has seen everything up to `publishedAt`. */
203
+ markSeen(publishedAt: string): void;
204
+ /**
205
+ * How many of `entries` are new to the visitor. On the very first visit
206
+ * nothing counts as new — call {@link SeenTracker.markSeen} with the newest
207
+ * entry then, so the next entry is the first one that counts.
208
+ */
209
+ unseen(entries: readonly {
210
+ publishedAt: string;
211
+ }[]): number;
212
+ }
213
+ /** The browser's `localStorage`, or null (server rendering, privacy mode). */
214
+ declare function browserStorage(): SeenStorage | null;
215
+ /**
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.
219
+ */
220
+ declare function createSeenTracker(workspace: string, storage?: SeenStorage | null): SeenTracker;
221
+ type ChangelogLocale = "de" | "en";
222
+ /** The category names the hosted page uses, for your own UI. */
223
+ declare const CATEGORY_LABELS: Record<ChangelogLocale, Record<ChangelogCategory, string>>;
224
+ /** The category colors of the hosted page and the dashboard (hex). */
225
+ declare const CATEGORY_COLORS: Record<ChangelogCategory, string>;
226
+ /** A category's name in German (`de*`) or English (everything else). */
227
+ declare function categoryLabel(category: ChangelogCategory, locale?: string): string;
228
+
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 };