wiki-formant 0.62.0 → 0.63.1

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
@@ -686,7 +686,7 @@ leafBlocks(blocks, BLOCK_SHAPE.containers);
686
686
 
687
687
  The dispatch over a repo's own block union stays in that repo; the bodies come from here. `coreAtomicText` has a prose body for every core leaf — the stats, link-grid and page-list bodies joined the originals when two of the three wikis turned out to extract nothing from them, so their MCP `get_page` answered a hub page as nearly empty.
688
688
 
689
- The core leaves — `content`, `codeTabs`, `banner`, `references`, `stats`, `linkGrid`, `recentPages`, `pageList` — had the same case bodies in every wiki's text export, markdown twin, validator and new-block record. A repo's switch hands those cases over and keeps its own, so it stays exhaustive over its union:
689
+ The core leaves — `content`, `codeTabs`, `banner`, `references`, `stats`, `linkGrid`, `testimonial`, `recentPages`, `pageList` — had the same case bodies in every wiki's text export, markdown twin, validator and new-block record. A repo's switch hands those cases over and keeps its own, so it stays exhaustive over its union:
690
690
 
691
691
  ```ts
692
692
  import { coreAtomicText, coreAtomicToMarkdown, coreBlockDefaults } from 'wiki-formant/blocks';
@@ -821,9 +821,11 @@ The dispatch stays a `switch` over the wiki's own union, so a new type is a comp
821
821
 
822
822
  An editor that draws each block in place, with a list of its own (miow's), hosts the same fields in `<BlockFieldsKit kit={KIT}>`, without `<BlockEditor>`. Only the two container fields need the full editor, since they hold a block list.
823
823
 
824
+ A `testimonial` is `{ quote, author, role?, avatarUrl? }`, drawn by `TestimonialView` as `figure.testimonial`: the words in a `blockquote`, then a `figcaption` holding the photo and a `span` of `strong` (who) and `small` (their role). radix-wiki and miow each had one under its own field names; a site with sized images passes `photo`, the `<img>` attributes (`src`, `srcSet`, `sizes`) it made from `avatarUrl`. They are data, because a server component renders the view.
825
+
824
826
  A banner with no text of its own shows `BANNER_MESSAGES` from `wiki-formant/text`, a sentence per variant that names no wiki; `BannerView` takes it unless `message` overrides it.
825
827
 
826
- The core blocks are `ContentBlock`, `CodeTabsBlock`, `StatsBlock`, `LinkGridBlock`, `ReferencesBlock`, `BannerBlock`, `ColumnsBlock<A>` and `InfoboxBlock<A>`, `A` being the wiki's atomic union. A wiki builds its union from them beside its own `recentPages` and `pageList`, which hold its own page rows or a section, and its native types.
828
+ The core blocks are `ContentBlock`, `CodeTabsBlock`, `StatsBlock`, `LinkGridBlock`, `ReferencesBlock`, `BannerBlock`, `TestimonialBlock`, `ColumnsBlock<A>` and `InfoboxBlock<A>`, `A` being the wiki's atomic union. A wiki builds its union from them beside its own `recentPages` and `pageList`, which hold its own page rows or a section, and its native types.
827
829
 
828
830
  ## Analytics
829
831
 
@@ -1,4 +1,4 @@
1
- import { type ReactNode } from 'react';
1
+ import { type ImgHTMLAttributes, type ReactNode } from 'react';
2
2
  import type { CodeTab, LinkGridGroup, ReferenceItem, ResolvedPageRef, StatItem } from './blocks.js';
3
3
  import type { WikiLinkComponent } from './react-server.js';
4
4
  export interface CodeTabsViewProps {
@@ -104,6 +104,19 @@ export declare function StatsView({ items, columns, }: {
104
104
  })[];
105
105
  columns?: number;
106
106
  }): import("react").JSX.Element | null;
107
+ export interface TestimonialViewProps {
108
+ quote: string;
109
+ author: string;
110
+ role?: string;
111
+ avatarUrl?: string;
112
+ /**
113
+ * The photo's sized `<img>` attributes, for a site with an image pipeline.
114
+ * Data, not a function: a server component renders this client view.
115
+ */
116
+ photo?: Pick<ImgHTMLAttributes<HTMLImageElement>, 'src' | 'srcSet' | 'sizes'>;
117
+ }
118
+ /** Someone's words over who said them, with their photo when there is one. */
119
+ export declare function TestimonialView({ quote, author, role, avatarUrl, photo }: TestimonialViewProps): import("react").JSX.Element | null;
107
120
  /**
108
121
  * A maintenance notice.
109
122
  *
@@ -114,6 +114,13 @@ export function StatsView({ items, columns = 4, }) {
114
114
  return null;
115
115
  return (_jsx("div", { className: cx('stat-grid', `stat-grid-${columns}`), children: rows.map((item, i) => (_jsxs("div", { className: "stat-card", children: [_jsxs("span", { className: "stat-value", children: [item.value, item.suffix && _jsx("span", { className: "stat-suffix", children: item.suffix })] }), _jsx("span", { className: "stat-label", children: item.label })] }, item.id ?? i))) }));
116
116
  }
117
+ /** Someone's words over who said them, with their photo when there is one. */
118
+ export function TestimonialView({ quote, author, role, avatarUrl, photo }) {
119
+ if (!quote.trim())
120
+ return null;
121
+ const src = avatarUrl && safeLinkHref(avatarUrl);
122
+ return (_jsxs("figure", { className: "testimonial", children: [_jsx("blockquote", { children: quote }), _jsxs("figcaption", { children: [src && _jsx("img", { src: src, ...photo, alt: "", loading: "lazy" }), _jsxs("span", { children: [_jsx("strong", { children: author }), role && _jsx("small", { children: role })] })] })] }));
123
+ }
117
124
  // ---- banner -----------------------------------------------------------------
118
125
  /**
119
126
  * A maintenance notice.
package/dist/blocks.d.ts CHANGED
@@ -94,6 +94,15 @@ export interface ReferencesBlock {
94
94
  title?: string;
95
95
  items: EditableReferenceItem[];
96
96
  }
97
+ /** Someone's words, as they said them, and who they are. */
98
+ export interface TestimonialBlock {
99
+ id: string;
100
+ type: 'testimonial';
101
+ quote: string;
102
+ author: string;
103
+ role?: string;
104
+ avatarUrl?: string;
105
+ }
97
106
  /** A Wikipedia-style notice. `text` replaces the variant's standard message. */
98
107
  export interface BannerBlock {
99
108
  id: string;
@@ -247,6 +256,11 @@ export type CoreAtomicBlock<P = ResolvedPageRef> = {
247
256
  type: 'linkGrid';
248
257
  intro?: string | null;
249
258
  groups: readonly LinkGridGroup[];
259
+ } | {
260
+ type: 'testimonial';
261
+ quote: string;
262
+ author: string;
263
+ role?: string | null;
250
264
  } | {
251
265
  type: 'recentPages' | 'pageList';
252
266
  resolvedPages?: readonly P[] | null;
@@ -346,6 +360,11 @@ export interface CoreBlockDefaults {
346
360
  type: 'banner';
347
361
  variant: 'stub';
348
362
  };
363
+ testimonial: () => {
364
+ type: 'testimonial';
365
+ quote: string;
366
+ author: string;
367
+ };
349
368
  }
350
369
  /**
351
370
  * The new-block factories for the core types, which all three wikis had typed
package/dist/blocks.js CHANGED
@@ -12,7 +12,7 @@
12
12
  // keeps this file free of any one repo's type union.
13
13
  import { htmlToMarkdown, inlineToMarkdown } from './markdown.js';
14
14
  import { relativeTime } from './freshness.js';
15
- import { BANNER_LABELS, bannerToText, codeTabsToText, linkGridToText, pageListToText, referencesToText, statsToText, stripHtml, } from './text.js';
15
+ import { BANNER_LABELS, bannerToText, codeTabsToText, linkGridToText, pageListToText, referencesToText, statsToText, stripHtml, testimonialToText, } from './text.js';
16
16
  // ---- the shapes the standard block types carry ------------------------------
17
17
  /**
18
18
  * The languages a code block or code tab offers, and the one a new one opens
@@ -188,6 +188,10 @@ export function statsToMarkdown(items) {
188
188
  return '';
189
189
  return items.map(s => `- **${s.value}${s.suffix ?? ''}** — ${s.label}`).join('\n');
190
190
  }
191
+ /** A quotation over its attribution. @internal */
192
+ export function testimonialToMarkdown(quote, author, role) {
193
+ return `> ${quote.trim().replace(/\n+/g, '\n> ')}\n>\n> — ${[author, role].filter(Boolean).join(', ')}`;
194
+ }
191
195
  /** Grouped links under their headings, with an optional lead paragraph. @internal */
192
196
  export function linkGridToMarkdown(groups, intro) {
193
197
  const lead = intro ? `${inlineToMarkdown(intro)}\n\n` : '';
@@ -216,6 +220,7 @@ export function coreAtomicText(block) {
216
220
  case 'references': return referencesToText(block.items);
217
221
  case 'stats': return statsToText(block.items);
218
222
  case 'linkGrid': return linkGridToText(block.groups, block.intro);
223
+ case 'testimonial': return testimonialToText(block.quote, block.author, block.role);
219
224
  case 'recentPages':
220
225
  case 'pageList': return pageListToText(block.resolvedPages ?? []);
221
226
  }
@@ -229,6 +234,7 @@ export function coreAtomicToMarkdown(block, opts) {
229
234
  case 'references': return referencesToMarkdown(block.items, block.title || opts.referencesTitle || 'References');
230
235
  case 'stats': return statsToMarkdown(block.items);
231
236
  case 'linkGrid': return linkGridToMarkdown(block.groups, block.intro);
237
+ case 'testimonial': return testimonialToMarkdown(block.quote, block.author, block.role);
232
238
  case 'recentPages':
233
239
  case 'pageList':
234
240
  return linkList((block.resolvedPages ?? []).map(p => ({
@@ -268,5 +274,6 @@ export function coreBlockDefaults(opts = {}) {
268
274
  linkGrid: () => ({ type: 'linkGrid', groups: [{ id: newId(), heading: 'Group', links: [] }] }),
269
275
  references: () => ({ type: 'references', title: referencesTitle, items: [] }),
270
276
  banner: () => ({ type: 'banner', variant: 'stub' }),
277
+ testimonial: () => ({ type: 'testimonial', quote: '', author: '' }),
271
278
  };
272
279
  }
package/dist/text.js CHANGED
@@ -107,6 +107,10 @@ export function bannerToText(label, text) {
107
107
  export function codeTabsToText(tabs) {
108
108
  return tabs.map(t => `[${t.label}]\n${t.code}`).join('\n');
109
109
  }
110
+ /** `“Their words” — Name, role`. @internal */
111
+ export function testimonialToText(quote, author, role) {
112
+ return `“${quote.trim()}” — ${[author, role].filter(Boolean).join(', ')}`;
113
+ }
110
114
  /** Metric cards, one per line: `99% Uptime`. @internal */
111
115
  export function statsToText(items) {
112
116
  return items.map(s => `${s.value}${s.suffix ?? ''} ${s.label}`).join('\n');
@@ -118,6 +118,8 @@ export function coreAtomicValidator(opts = {}) {
118
118
  return validateReferenceItems(b.items);
119
119
  case 'linkGrid':
120
120
  return validateLinkGroups(b.groups);
121
+ case 'testimonial':
122
+ return typeof b.quote === 'string' && typeof b.author === 'string' && optionalString(b.role) && optionalString(b.avatarUrl);
121
123
  default:
122
124
  return false;
123
125
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-formant",
3
- "version": "0.62.0",
3
+ "version": "0.63.1",
4
4
  "description": "The portable half of a wiki: derived taxonomy and facet controls, a version-negotiating MCP transport, markdown twins, block rendering, a rich-text editor engine, conditional-GET plumbing, and the headers and CSP every origin sends.",
5
5
  "type": "module",
6
6
  "license": "MIT",