@solutiondesk/help 1.0.0-a1101b8 → 1.0.0-a6af2d6

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
@@ -29,7 +29,7 @@ const { page } = await help.page("guides/login"); // one page with its text
29
29
 
30
30
  | Method | API | Returns |
31
31
  | --- | --- | --- |
32
- | `site({ lang })` | `GET …/site` | The site (`name`, `languages`, `websiteUrl`, its own building `blocks`) and its navigation tree: pages, folders (with index page, `root` tab, `group`, `defaultOpen`, `collapsible`), separators and links. Empty folders are left out. |
32
+ | `site({ lang })` | `GET …/site` | The site (`name`, `languages`, `websiteUrl`, its own building `blocks`) and its navigation tree: pages, folders (with index page, `listIndex`, `root` tab, `description`, `group`, `defaultOpen`, `collapsible`), separators and links. Empty folders are left out. |
33
33
  | `pages({ lang })` | `GET …/pages` | Every visible page without its text, in reading order (for builds and sitemaps). |
34
34
  | `page(path, { lang, redirect })` | `GET …/page?path=` | One page: `markdown`, `html`, `headings`, `blocks`, `customBlocks`, `breadcrumbs`, `previous`/`next`. The home page has the path `""`. |
35
35
  | `pageMarkdown(path, { lang })` | `GET …/page.md?path=` | One page as plain Markdown. |
@@ -37,7 +37,7 @@ const { page } = await help.page("guides/login"); // one page with its text
37
37
  | `preview(token, { lang })` | `GET …/preview?token=` | A page in any state (drafts too) for your preview route. |
38
38
  | `llmsTxt({ lang })` / `llmsFullTxt({ lang })` | `GET …/llms.txt`, `…/llms-full.txt` | The site for language models: the index, or every page as Markdown. |
39
39
 
40
- `html` is safe to insert as it is: raw HTML in the Markdown is shown as text, links only go to web addresses, `mailto:`, paths and anchors, images only come from your help center. Headings carry the `id`s listed in `headings`.
40
+ `html` is safe to insert as it is: raw HTML in the Markdown is shown as text, links only go to web addresses, `mailto:`, paths and anchors, images only come from your help center. Headings carry the `id`s listed in `headings`, made the way GitHub and Fumadocs make them (umlauts kept, `-1` for a repeat), so a site moved from Fumadocs keeps its `#links`; a step's title is a heading too.
41
41
 
42
42
  ### Moved pages
43
43
 
@@ -104,7 +104,7 @@ The navigation becomes the files Fumadocs knows from a content folder:
104
104
  | --- | --- |
105
105
  | page `guides/login` | `guides/login.mdx` (`slugs: ["guides", "login"]`) |
106
106
  | home page | `index.mdx` at the top, or `(core)/index.mdx` when it is a top-level group's own page |
107
- | folder `guides` | `guides/meta.json` with `title`, `icon`, `root`, `defaultOpen`, `collapsible`, `pages` |
107
+ | folder `guides` | `guides/meta.json` with `title`, `description` (shown under a tab's name), `pagesIndex` — or, with `listIndex`, `"index"` first in `pages` (after a leading separator), so the page is an entry and the name only opens the folder —, `icon`, `root`, `defaultOpen`, `collapsible`, `pages` |
108
108
  | group folder `core` (adds no address part) | `(core)/` — a Fumadocs folder group, so `(core)/sign-up/index.mdx` is still `sign-up` |
109
109
  | folder's own page | `guides/index.mdx` (`pagesIndex: "index"`, so it also works for `root` tabs) |
110
110
  | any other page with the slug `index` | `guides/index_.mdx` (`slugs: ["guides", "index"]`), so it never collides with the folder's own page |
@@ -286,7 +286,7 @@ document.querySelector("article")!.innerHTML = page.html;
286
286
  | Block | HTML |
287
287
  | --- | --- |
288
288
  | callout | `<div class="sd-callout" data-type="info\|warn\|error\|success\|idea" role="note">` with `<p class="sd-callout-title">` and `<div class="sd-callout-body">` |
289
- | steps | `<ol class="sd-steps">` of `<li class="sd-step">` with `<p class="sd-step-title">` and `<div class="sd-step-body">` |
289
+ | steps | `<ol class="sd-steps">` of `<li class="sd-step">` with `<h3 class="sd-step-title" id="…">` and `<div class="sd-step-body">` |
290
290
  | tabs | `<div class="sd-tabs">` of `<section class="sd-tab" data-title="…">` (build the tab buttons from `data-title`; `page.blocks` says when a page has tabs) |
291
291
  | cards | `<div class="sd-cards">` of `<a class="sd-card" href="…" data-icon="Rocket">` (or `<div class="sd-card">`) with `<p class="sd-card-title">` and `<div class="sd-card-body">` |
292
292
  | titled code | `<figure class="sd-code"><figcaption>client.ts</figcaption><pre><code class="language-ts">` |
@@ -81,6 +81,8 @@ interface HelpPublicTreeFolder {
81
81
  /** The folder's path (its index page, if any, lives here; a group's is its parent's). */
82
82
  path: string;
83
83
  title: string;
84
+ /** A short line under a tab's name in the tab menu (Fumadocs `description`); null without one. */
85
+ description: string | null;
84
86
  icon: string | null;
85
87
  /** A tab (top level only; Fumadocs `root`). */
86
88
  root: boolean;
@@ -93,6 +95,11 @@ interface HelpPublicTreeFolder {
93
95
  collapsible: boolean;
94
96
  /** The folder's own page, shown at the folder's path; null without one. */
95
97
  index: HelpPublicTreePage | null;
98
+ /**
99
+ * List `index` as the folder's first entry (after a section title it starts
100
+ * with); the folder's name then only opens it (Fumadocs: `index` in `pages`).
101
+ */
102
+ listIndex: boolean;
96
103
  children: HelpPublicTreeNode[];
97
104
  }
98
105
  /** A labelled divider between entries (Fumadocs `---Title---`). */
@@ -318,6 +325,8 @@ interface HelpStepsProps {
318
325
  }
319
326
  interface HelpStepProps {
320
327
  title?: string;
328
+ /** The title's anchor id (a step's title is a heading of the page, level 3). */
329
+ id?: string;
321
330
  children: ReactNode;
322
331
  }
323
332
  interface HelpTabsProps {
@@ -664,6 +673,8 @@ interface HelpPageData extends PageData {
664
673
  /** What every folder's `meta.json` of a help source carries. */
665
674
  interface HelpMetaData extends MetaData {
666
675
  title?: string;
676
+ /** Shown under a tab's name in Fumadocs' tab menu. */
677
+ description?: string;
667
678
  icon?: string;
668
679
  root?: boolean;
669
680
  defaultOpen?: boolean;
package/dist/fumadocs.js CHANGED
@@ -112,28 +112,25 @@ function codeInfo(lang, meta) {
112
112
  const title = (match ? match[1] ?? match[2] ?? match[3] : void 0)?.trim() || void 0;
113
113
  return { language, title };
114
114
  }
115
- var TRANSLITERATIONS = {
116
- \u00E4: "ae",
117
- \u00F6: "oe",
118
- \u00FC: "ue",
119
- \u00DF: "ss",
120
- \u00E6: "ae",
121
- \u0153: "oe",
122
- \u00F8: "o",
123
- \u00E5: "a",
124
- \u0111: "d",
125
- \u00F0: "d",
126
- \u00FE: "th",
127
- \u0142: "l",
128
- \u0131: "i"
129
- };
130
- function headingSlug(text) {
131
- return text.normalize("NFC").toLowerCase().replace(/[äöüßæœøåđðþłı]/g, (ch) => TRANSLITERATIONS[ch] ?? ch).normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 96).replace(/-+$/, "");
115
+ var GITHUB_SLUG_DROP = /[^\p{L}\p{M}\p{N}\p{Pc} -]/gu;
116
+ function headingId(text, used) {
117
+ const base = text.toLowerCase().replace(GITHUB_SLUG_DROP, "").replace(/ /g, "-") || "section";
118
+ let id = base;
119
+ while (used.has(id)) {
120
+ const count = (used.get(base) ?? 0) + 1;
121
+ used.set(base, count);
122
+ id = `${base}-${count}`;
123
+ }
124
+ used.set(id, 0);
125
+ return id;
126
+ }
127
+ function isTitledStep(node) {
128
+ return node.type === "containerDirective" && node.name === "step" && !!directiveTitle(node);
132
129
  }
133
130
  function collectHeadings(root) {
134
131
  const out = [];
135
132
  const walk = (node) => {
136
- if (node.type === "heading") out.push(node);
133
+ if (node.type === "heading" || isTitledStep(node)) out.push(node);
137
134
  if ("children" in node) for (const child of node.children) walk(child);
138
135
  };
139
136
  walk(root);
@@ -149,12 +146,14 @@ function headingIds(root, known, override) {
149
146
  return ids;
150
147
  }
151
148
  const used = /* @__PURE__ */ new Map();
149
+ const withoutBlocks = override ? (node) => {
150
+ if (override(node) === void 0) return void 0;
151
+ if (node.type !== "textDirective") return "";
152
+ return node.children.map((child) => plainText(child)).join("").trim();
153
+ } : void 0;
152
154
  for (const heading of headings) {
153
- const text = plainText(heading, override).replace(/\s+/g, " ").trim();
154
- const base = headingSlug(text) || "section";
155
- const seen = used.get(base) ?? 0;
156
- used.set(base, seen + 1);
157
- ids.set(heading, seen === 0 ? base : `${base}-${seen + 1}`);
155
+ const text = heading.type === "heading" ? plainText(heading, withoutBlocks) : (directiveTitle(heading) ?? "").replace(/\s+/g, " ").trim();
156
+ ids.set(heading, headingId(text, used));
158
157
  }
159
158
  return ids;
160
159
  }
@@ -379,8 +378,8 @@ var PLAIN_CODE_COLORS = {
379
378
  var helpBodyComponents = {
380
379
  Callout: ({ type, title, children }) => /* @__PURE__ */ jsx(Callout, { type, title, children }),
381
380
  Steps: ({ children }) => /* @__PURE__ */ jsx(Steps, { children }),
382
- Step: ({ title, children }) => /* @__PURE__ */ jsxs(Step, { children: [
383
- title ? /* @__PURE__ */ jsx("h3", { children: title }) : null,
381
+ Step: ({ title, id, children }) => /* @__PURE__ */ jsxs(Step, { children: [
382
+ title ? /* @__PURE__ */ jsx(Heading, { as: "h3", id, children: title }) : null,
384
383
  children
385
384
  ] }),
386
385
  Tabs: ({ items, children }) => /* @__PURE__ */ jsx(Tabs, { items, children }),
@@ -555,7 +554,7 @@ function renderDirective(node, ctx) {
555
554
  return /* @__PURE__ */ jsx(c.Steps, { children: content.map(
556
555
  (child, i) => isDirective(child, "step") ? (
557
556
  // biome-ignore lint/suspicious/noArrayIndexKey: static content, never reordered.
558
- /* @__PURE__ */ jsx(c.Step, { title: directiveTitle(child), children: renderAll(contentOf(child), ctx) }, i)
557
+ /* @__PURE__ */ jsx(c.Step, { title: directiveTitle(child), id: ctx.ids.get(child), children: renderAll(contentOf(child), ctx) }, i)
559
558
  ) : (
560
559
  // A loose block between steps becomes a step of its own.
561
560
  // biome-ignore lint/suspicious/noArrayIndexKey: static content, never reordered.
@@ -563,7 +562,7 @@ function renderDirective(node, ctx) {
563
562
  )
564
563
  ) });
565
564
  case "step":
566
- return /* @__PURE__ */ jsx(c.Steps, { children: /* @__PURE__ */ jsx(c.Step, { title, children: renderAll(content, ctx) }) });
565
+ return /* @__PURE__ */ jsx(c.Steps, { children: /* @__PURE__ */ jsx(c.Step, { title, id: ctx.ids.get(node), children: renderAll(content, ctx) }) });
567
566
  case "tabs":
568
567
  return renderTabs(content, ctx);
569
568
  case "tab":
@@ -792,6 +791,13 @@ function helpStructuredData(markdown, headings, blocks) {
792
791
  if (current && content) out.headings.push({ id: current, content });
793
792
  return;
794
793
  }
794
+ if (isTitledStep(node)) {
795
+ current = ids.get(node);
796
+ const content = directiveTitle(node) ?? "";
797
+ if (current) out.headings.push({ id: current, content });
798
+ for (const child of contentOf(node)) walk(child);
799
+ return;
800
+ }
795
801
  const use = matcher.use(node);
796
802
  if (use) {
797
803
  add(use.text);
@@ -864,16 +870,23 @@ function helpTreeToFiles(payload, options) {
864
870
  };
865
871
  files.push({ type: "page", path: file, slugs: slugsOf(page.path), data });
866
872
  };
873
+ const folderPages = (folder) => {
874
+ const pages = folder.children.map(helpMetaEntry);
875
+ if (!folder.index || !folder.listIndex) return pages;
876
+ const at = folder.children[0]?.type === "separator" ? 1 : 0;
877
+ return [...pages.slice(0, at), "index", ...pages.slice(at)];
878
+ };
867
879
  const addFolder = (folder, dir) => {
868
880
  if (folder.index) addPage(folder.index, join(dir, ext("index", "mdx")));
869
881
  const data = {
870
882
  title: folder.title,
883
+ ...folder.description ? { description: folder.description } : {},
871
884
  ...folder.icon ? { icon: folder.icon } : {},
872
885
  ...folder.root ? { root: true } : {},
873
886
  defaultOpen: folder.defaultOpen,
874
887
  collapsible: folder.collapsible,
875
- ...folder.index ? { pagesIndex: "index" } : {},
876
- pages: folder.children.map(helpMetaEntry)
888
+ ...folder.index && !folder.listIndex ? { pagesIndex: "index" } : {},
889
+ pages: folderPages(folder)
877
890
  };
878
891
  files.push({ type: "meta", path: join(dir, ext("meta", "json")), data });
879
892
  addChildren(folder.children, dir);
package/dist/index.d.cts CHANGED
@@ -208,6 +208,8 @@ interface HelpPublicTreeFolder {
208
208
  /** The folder's path (its index page, if any, lives here; a group's is its parent's). */
209
209
  path: string;
210
210
  title: string;
211
+ /** A short line under a tab's name in the tab menu (Fumadocs `description`); null without one. */
212
+ description: string | null;
211
213
  icon: string | null;
212
214
  /** A tab (top level only; Fumadocs `root`). */
213
215
  root: boolean;
@@ -220,6 +222,11 @@ interface HelpPublicTreeFolder {
220
222
  collapsible: boolean;
221
223
  /** The folder's own page, shown at the folder's path; null without one. */
222
224
  index: HelpPublicTreePage | null;
225
+ /**
226
+ * List `index` as the folder's first entry (after a section title it starts
227
+ * with); the folder's name then only opens it (Fumadocs: `index` in `pages`).
228
+ */
229
+ listIndex: boolean;
223
230
  children: HelpPublicTreeNode[];
224
231
  }
225
232
  /** A labelled divider between entries (Fumadocs `---Title---`). */
package/dist/index.d.ts CHANGED
@@ -208,6 +208,8 @@ interface HelpPublicTreeFolder {
208
208
  /** The folder's path (its index page, if any, lives here; a group's is its parent's). */
209
209
  path: string;
210
210
  title: string;
211
+ /** A short line under a tab's name in the tab menu (Fumadocs `description`); null without one. */
212
+ description: string | null;
211
213
  icon: string | null;
212
214
  /** A tab (top level only; Fumadocs `root`). */
213
215
  root: boolean;
@@ -220,6 +222,11 @@ interface HelpPublicTreeFolder {
220
222
  collapsible: boolean;
221
223
  /** The folder's own page, shown at the folder's path; null without one. */
222
224
  index: HelpPublicTreePage | null;
225
+ /**
226
+ * List `index` as the folder's first entry (after a section title it starts
227
+ * with); the folder's name then only opens it (Fumadocs: `index` in `pages`).
228
+ */
229
+ listIndex: boolean;
223
230
  children: HelpPublicTreeNode[];
224
231
  }
225
232
  /** A labelled divider between entries (Fumadocs `---Title---`). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solutiondesk/help",
3
- "version": "1.0.0-a1101b8",
3
+ "version": "1.0.0-a6af2d6",
4
4
  "description": "Typed, zero-dependency client for the SolutionDesk public help center API, plus a Fumadocs adapter: navigation, pages, search, previews, llms.txt and webhook verification.",
5
5
  "keywords": [
6
6
  "solutiondesk",