@backmatter/core 0.0.25 → 0.0.26

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backmatter/core",
3
- "version": "0.0.25",
3
+ "version": "0.0.26",
4
4
  "description": "Experimental Backmatter SDK. APIs will break. Not ready for production.",
5
5
  "type": "module",
6
6
  "publishConfig": {
package/src/config.ts CHANGED
@@ -7,6 +7,8 @@ import { detectGitBranch } from "./sync-schema";
7
7
  export type DevBarHostConfig = {
8
8
  /** Draw a viewport ring and top pill while the site session is in preview. */
9
9
  previewFrame?: boolean;
10
+ /** Bottom corner radius (px) for the preview ring (host window lip below the chrome). */
11
+ previewFrameRadius?: number;
10
12
  };
11
13
 
12
14
  export type BackmatterDirs = {
package/src/index.ts CHANGED
@@ -10,6 +10,8 @@ export type {
10
10
  export { field, serializeField, parseFieldSnapshot } from "./fields";
11
11
  export {
12
12
  MAX_MEDIA_BYTES,
13
+ MAX_MEDIA_TEXT,
14
+ MAX_VARIANT_WIDTH,
13
15
  MEDIA_CONTENT_TYPES,
14
16
  MEDIA_WIDTHS,
15
17
  isMediaContentType,
@@ -92,6 +94,14 @@ export {
92
94
  sortByRouteSpecificity,
93
95
  } from "./routes";
94
96
 
97
+ export type { PageNavDoc, PageNavItem } from "./page-nav";
98
+ export {
99
+ PAGE_NAV_DEFAULT_LIMIT,
100
+ PAGE_NAV_MAX_LIMIT,
101
+ pageNavItems,
102
+ paginatePageNavItems,
103
+ } from "./page-nav";
104
+
95
105
  export type { PathMatch, ContentSource, ParsedContentKey } from "./path";
96
106
  export {
97
107
  compilePathPattern,
package/src/media-url.ts CHANGED
@@ -1,13 +1,19 @@
1
1
  // packages/core/src/media-url.ts
2
2
 
3
- /** Widths the catalog lists for the gallery. A public URL may use any positive width. */
3
+ /** Widths the catalog lists for the gallery. */
4
4
  export const MEDIA_WIDTHS = [400, 800, 1200, 2000] as const;
5
5
 
6
+ /** Widest variant the CDN will resize. A wider request is refused. */
7
+ export const MAX_VARIANT_WIDTH = 2000;
8
+
6
9
  export type MediaWidth = (typeof MEDIA_WIDTHS)[number];
7
10
 
8
11
  /** Bytes the catalog accepts for one file. */
9
12
  export const MAX_MEDIA_BYTES = 20 * 1024 * 1024;
10
13
 
14
+ /** Characters of alt text or caption the catalog stores on one image. */
15
+ export const MAX_MEDIA_TEXT = 500;
16
+
11
17
  export const MEDIA_CONTENT_TYPES = [
12
18
  "image/jpeg",
13
19
  "image/png",
@@ -39,7 +45,9 @@ export function mediaVariantUrl(
39
45
  id: string,
40
46
  width: number,
41
47
  ): string | null {
42
- if (!isMediaId(id) || !Number.isInteger(width) || width < 1) return null;
48
+ if (!isMediaId(id) || !Number.isInteger(width) || width < 1 || width > MAX_VARIANT_WIDTH) {
49
+ return null;
50
+ }
43
51
  return `${origin.replace(/\/$/, "")}/img/${id}/width=${width},format=auto`;
44
52
  }
45
53
 
@@ -112,7 +112,7 @@ async function media(
112
112
  if (flags.width) {
113
113
  const width = Number(flags.width);
114
114
  const url = mediaVariantUrl(origin, id, width);
115
- if (!url) throw new Error("width must be a positive integer");
115
+ if (!url) throw new Error("width must be a positive integer up to 2000");
116
116
  io.log(url);
117
117
  return 0;
118
118
  }
@@ -0,0 +1,67 @@
1
+ // packages/core/src/page-nav.ts
2
+ import { isRoutable, routeFor, type RouteModel } from "./routes";
3
+
4
+ export type PageNavDoc = {
5
+ locale: string;
6
+ type: string;
7
+ slug: string;
8
+ };
9
+
10
+ export type PageNavItem = {
11
+ key: string;
12
+ label: string;
13
+ href: string;
14
+ current: boolean;
15
+ };
16
+
17
+ /** Routable documents, in label order. Unroutable models stay out of the menu. */
18
+ export function pageNavItems(input: {
19
+ docs: readonly PageNavDoc[];
20
+ models: readonly RouteModel[];
21
+ current: PageNavDoc | null;
22
+ }): PageNavItem[] {
23
+ const byType = new Map(input.models.map((model) => [model.type, model]));
24
+ const locales = new Set(input.docs.map((doc) => doc.locale));
25
+ const showLocale = locales.size > 1;
26
+ const items: PageNavItem[] = [];
27
+ for (const doc of input.docs) {
28
+ const model = byType.get(doc.type);
29
+ if (!model || !isRoutable(model)) continue;
30
+ const href = routeFor(model, doc.locale, doc.slug);
31
+ if (!href) continue;
32
+ const name = `${doc.type}/${doc.slug}`;
33
+ items.push({
34
+ key: `${doc.locale}/${doc.type}/${doc.slug}`,
35
+ label: showLocale ? `${name} · ${doc.locale}` : name,
36
+ href,
37
+ current: Boolean(
38
+ input.current &&
39
+ input.current.locale === doc.locale &&
40
+ input.current.type === doc.type &&
41
+ input.current.slug === doc.slug,
42
+ ),
43
+ });
44
+ }
45
+ items.sort((a, b) => a.label.localeCompare(b.label));
46
+ return items;
47
+ }
48
+
49
+ export const PAGE_NAV_DEFAULT_LIMIT = 10;
50
+ export const PAGE_NAV_MAX_LIMIT = 50;
51
+
52
+ export function paginatePageNavItems(
53
+ items: readonly PageNavItem[],
54
+ query: { q?: string; limit?: number; offset?: number },
55
+ ): { items: PageNavItem[]; total: number } {
56
+ const needle = query.q?.trim().toLowerCase() ?? "";
57
+ const filtered = needle ? items.filter((item) => item.label.toLowerCase().includes(needle)) : [...items];
58
+ const limit = Math.min(
59
+ PAGE_NAV_MAX_LIMIT,
60
+ Math.max(1, query.limit ?? PAGE_NAV_DEFAULT_LIMIT),
61
+ );
62
+ const offset = Math.max(0, query.offset ?? 0);
63
+ return {
64
+ items: filtered.slice(offset, offset + limit),
65
+ total: filtered.length,
66
+ };
67
+ }