notionic 0.0.0-stage → 0.8.3
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/CHANGELOG.md +135 -0
- package/LICENSE +21 -0
- package/README.md +261 -2
- package/dist/fetch.d.ts +62 -0
- package/dist/fetch.js +259 -0
- package/dist/files-DIc0m6LM.js +104 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +1266 -0
- package/dist/markdown.d.ts +15 -0
- package/dist/markdown.js +162 -0
- package/dist/page-BmeweSIt.js +27 -0
- package/dist/styles.css +108 -0
- package/dist/types-DOxr5KpT.d.ts +1305 -0
- package/dist/utils-BL_oAR33.js +18 -0
- package/package.json +73 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,1266 @@
|
|
|
1
|
+
import { a as resolvePageLink, i as resolveLink, n as defaultPageHref, r as normalizePageId, t as resolveFileUrl } from "./files-DIc0m6LM.js";
|
|
2
|
+
import { n as plainText, r as warnOnce, t as cx } from "./utils-BL_oAR33.js";
|
|
3
|
+
import { t as pageTitle } from "./page-BmeweSIt.js";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/colors.ts
|
|
6
|
+
const TEXT = {
|
|
7
|
+
gray: "text-(--notionic-gray)",
|
|
8
|
+
brown: "text-(--notionic-brown)",
|
|
9
|
+
orange: "text-(--notionic-orange)",
|
|
10
|
+
yellow: "text-(--notionic-yellow)",
|
|
11
|
+
green: "text-(--notionic-green)",
|
|
12
|
+
blue: "text-(--notionic-blue)",
|
|
13
|
+
purple: "text-(--notionic-purple)",
|
|
14
|
+
pink: "text-(--notionic-pink)",
|
|
15
|
+
red: "text-(--notionic-red)"
|
|
16
|
+
};
|
|
17
|
+
const BACKGROUND = {
|
|
18
|
+
gray_background: "bg-(--notionic-gray-bg)",
|
|
19
|
+
brown_background: "bg-(--notionic-brown-bg)",
|
|
20
|
+
orange_background: "bg-(--notionic-orange-bg)",
|
|
21
|
+
yellow_background: "bg-(--notionic-yellow-bg)",
|
|
22
|
+
green_background: "bg-(--notionic-green-bg)",
|
|
23
|
+
blue_background: "bg-(--notionic-blue-bg)",
|
|
24
|
+
purple_background: "bg-(--notionic-purple-bg)",
|
|
25
|
+
pink_background: "bg-(--notionic-pink-bg)",
|
|
26
|
+
red_background: "bg-(--notionic-red-bg)"
|
|
27
|
+
};
|
|
28
|
+
/** The class for a Notion color (`red`, `red_background`…), or undefined for `default`. */
|
|
29
|
+
function colorClass(color) {
|
|
30
|
+
if (!color) return void 0;
|
|
31
|
+
return TEXT[color] ?? BACKGROUND[color];
|
|
32
|
+
}
|
|
33
|
+
function isBackgroundColor(color) {
|
|
34
|
+
return Boolean(color && color in BACKGROUND);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A block's color: text color, or a background that bleeds past the text, so
|
|
38
|
+
* the text stays on the same edge as the blocks around it. Blocks with a
|
|
39
|
+
* frame of their own (quote, callout) use `colorClass` instead.
|
|
40
|
+
*/
|
|
41
|
+
function blockColorClass(color) {
|
|
42
|
+
const cls = colorClass(color);
|
|
43
|
+
if (!cls) return void 0;
|
|
44
|
+
return isBackgroundColor(color) ? `${cls} -mx-2 rounded-md px-2 py-1` : cls;
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/rich-text.tsx
|
|
48
|
+
const CODE_CLASSES = "rounded-sm bg-muted px-1.5 py-0.5 font-mono text-[0.875em] box-decoration-clone";
|
|
49
|
+
const NOWRAP_CODE_LENGTH = 24;
|
|
50
|
+
const LINK_CLASSES = "font-medium underline underline-offset-4 decoration-from-font decoration-muted-foreground/50 transition-[text-decoration-color] hover:decoration-foreground rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring";
|
|
51
|
+
/** An anchor with notionic's link styling; external links open in a new tab. */
|
|
52
|
+
function NotionLink({ link, className, children }) {
|
|
53
|
+
return /* @__PURE__ */ jsxs("a", {
|
|
54
|
+
href: link.href,
|
|
55
|
+
...link.external ? {
|
|
56
|
+
target: "_blank",
|
|
57
|
+
rel: "noopener noreferrer"
|
|
58
|
+
} : {},
|
|
59
|
+
className: cx(LINK_CLASSES, className),
|
|
60
|
+
children: [children, link.external ? /* @__PURE__ */ jsx("span", {
|
|
61
|
+
className: "sr-only",
|
|
62
|
+
children: " (opens in a new tab)"
|
|
63
|
+
}) : null]
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function RichTextItem({ item, context }) {
|
|
67
|
+
const { annotations } = item;
|
|
68
|
+
let node = item.plain_text.includes("\n") ? item.plain_text.split("\n").flatMap((line, i) => i === 0 ? [line] : [/* @__PURE__ */ jsx("br", {}, i), line]) : item.plain_text;
|
|
69
|
+
if (item.type === "mention" && item.mention.type === "date") node = /* @__PURE__ */ jsx("time", {
|
|
70
|
+
dateTime: item.mention.date.start,
|
|
71
|
+
children: node
|
|
72
|
+
});
|
|
73
|
+
if (item.type === "equation") {
|
|
74
|
+
const InlineEquation = context.components.inline_equation;
|
|
75
|
+
if (InlineEquation) node = /* @__PURE__ */ jsx(InlineEquation, {
|
|
76
|
+
expression: item.equation.expression,
|
|
77
|
+
context
|
|
78
|
+
});
|
|
79
|
+
} else if (annotations.code) {
|
|
80
|
+
const short = item.plain_text.length <= NOWRAP_CODE_LENGTH;
|
|
81
|
+
node = /* @__PURE__ */ jsx("code", {
|
|
82
|
+
className: cx(CODE_CLASSES, short && "whitespace-nowrap"),
|
|
83
|
+
children: node
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (annotations.bold) node = /* @__PURE__ */ jsx("strong", {
|
|
87
|
+
className: "font-semibold",
|
|
88
|
+
children: node
|
|
89
|
+
});
|
|
90
|
+
if (annotations.italic) node = /* @__PURE__ */ jsx("em", { children: node });
|
|
91
|
+
if (annotations.strikethrough) node = /* @__PURE__ */ jsx("s", { children: node });
|
|
92
|
+
if (annotations.underline) node = /* @__PURE__ */ jsx("u", {
|
|
93
|
+
className: "underline-offset-4 decoration-from-font",
|
|
94
|
+
children: node
|
|
95
|
+
});
|
|
96
|
+
const color = colorClass(annotations.color);
|
|
97
|
+
if (color) node = isBackgroundColor(annotations.color) ? /* @__PURE__ */ jsx("mark", {
|
|
98
|
+
className: cx("rounded-sm px-0.5 text-inherit box-decoration-clone", color),
|
|
99
|
+
children: node
|
|
100
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
101
|
+
className: color,
|
|
102
|
+
children: node
|
|
103
|
+
});
|
|
104
|
+
const link = resolveLink(item.href, context);
|
|
105
|
+
if (link) node = /* @__PURE__ */ jsx(NotionLink, {
|
|
106
|
+
link,
|
|
107
|
+
children: node
|
|
108
|
+
});
|
|
109
|
+
return node;
|
|
110
|
+
}
|
|
111
|
+
/** A Notion rich text array: annotations, colors, links and mentions. */
|
|
112
|
+
function RichText({ text, context }) {
|
|
113
|
+
return text.map((item, i) => /* @__PURE__ */ jsx(RichTextItem, {
|
|
114
|
+
item,
|
|
115
|
+
context
|
|
116
|
+
}, i));
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/blocks/code.tsx
|
|
120
|
+
const LANGUAGE_NAMES = {
|
|
121
|
+
"c#": "C#",
|
|
122
|
+
"c++": "C++",
|
|
123
|
+
coffeescript: "CoffeeScript",
|
|
124
|
+
css: "CSS",
|
|
125
|
+
glsl: "GLSL",
|
|
126
|
+
graphql: "GraphQL",
|
|
127
|
+
html: "HTML",
|
|
128
|
+
javascript: "JavaScript",
|
|
129
|
+
json: "JSON",
|
|
130
|
+
latex: "LaTeX",
|
|
131
|
+
"objective-c": "Objective-C",
|
|
132
|
+
ocaml: "OCaml",
|
|
133
|
+
php: "PHP",
|
|
134
|
+
powershell: "PowerShell",
|
|
135
|
+
scss: "SCSS",
|
|
136
|
+
sql: "SQL",
|
|
137
|
+
typescript: "TypeScript",
|
|
138
|
+
vb: "VB",
|
|
139
|
+
"vb.net": "VB.NET",
|
|
140
|
+
webassembly: "WebAssembly",
|
|
141
|
+
xml: "XML",
|
|
142
|
+
yaml: "YAML"
|
|
143
|
+
};
|
|
144
|
+
/** A Notion code language as people write it: "typescript" is "TypeScript". */
|
|
145
|
+
function languageName(language) {
|
|
146
|
+
return LANGUAGE_NAMES[language] ?? language.charAt(0).toUpperCase() + language.slice(1);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Plain code, no highlighting. For syntax highlighting and a copy button,
|
|
150
|
+
* install the shadcn version: `npx shadcn add @notionic/notion-code`.
|
|
151
|
+
*/
|
|
152
|
+
function Code({ block, context }) {
|
|
153
|
+
const { language, rich_text, caption } = block.code;
|
|
154
|
+
const code = plainText(rich_text);
|
|
155
|
+
const name = languageName(language);
|
|
156
|
+
return /* @__PURE__ */ jsxs("figure", {
|
|
157
|
+
className: "overflow-hidden rounded-lg border border-border bg-muted",
|
|
158
|
+
children: [
|
|
159
|
+
/* @__PURE__ */ jsx("div", {
|
|
160
|
+
className: "border-b border-border px-4 py-2 text-xs text-muted-foreground",
|
|
161
|
+
children: name
|
|
162
|
+
}),
|
|
163
|
+
/* @__PURE__ */ jsx("pre", {
|
|
164
|
+
tabIndex: 0,
|
|
165
|
+
"aria-label": `Code, ${name}`,
|
|
166
|
+
className: "overflow-x-auto px-4 py-3 font-mono text-[0.8125rem] leading-relaxed focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring",
|
|
167
|
+
children: /* @__PURE__ */ jsx("code", { children: code })
|
|
168
|
+
}),
|
|
169
|
+
caption.length > 0 ? /* @__PURE__ */ jsx("figcaption", {
|
|
170
|
+
className: "border-t border-border px-4 py-2 text-sm text-muted-foreground",
|
|
171
|
+
children: /* @__PURE__ */ jsx(RichText, {
|
|
172
|
+
text: caption,
|
|
173
|
+
context
|
|
174
|
+
})
|
|
175
|
+
}) : null
|
|
176
|
+
]
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/blocks/disclosure.tsx
|
|
181
|
+
/**
|
|
182
|
+
* The chevron of a disclosure (toggle, toggleable heading): one line tall, so
|
|
183
|
+
* it lines up with the first line of text, and rotated while open. Put it in a
|
|
184
|
+
* `<summary>` inside a `<details className="group/toggle">`.
|
|
185
|
+
*/
|
|
186
|
+
function DisclosureChevron() {
|
|
187
|
+
return /* @__PURE__ */ jsx("span", {
|
|
188
|
+
"aria-hidden": "true",
|
|
189
|
+
className: "flex h-[1lh] shrink-0 items-center text-muted-foreground",
|
|
190
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
191
|
+
"aria-hidden": "true",
|
|
192
|
+
viewBox: "0 0 24 24",
|
|
193
|
+
fill: "none",
|
|
194
|
+
stroke: "currentColor",
|
|
195
|
+
strokeWidth: "2",
|
|
196
|
+
strokeLinecap: "round",
|
|
197
|
+
strokeLinejoin: "round",
|
|
198
|
+
className: "size-4 transition-transform duration-150 ease-out group-open/toggle:rotate-90 motion-reduce:transition-none",
|
|
199
|
+
children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" })
|
|
200
|
+
})
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/** Summary classes: a row with the chevron, and no native marker. */
|
|
204
|
+
const SUMMARY_CLASSES = "flex cursor-pointer list-none gap-1.5 rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring [&::-webkit-details-marker]:hidden";
|
|
205
|
+
/**
|
|
206
|
+
* `<details>` classes: the content grows from zero height and fades in, and
|
|
207
|
+
* shrinks back when closed. Browsers without `::details-content` or
|
|
208
|
+
* `interpolate-size` open instantly; reduced motion keeps only the fade.
|
|
209
|
+
*/
|
|
210
|
+
const DISCLOSURE_CLASSES = "[interpolate-size:allow-keywords] details-content:h-0 details-content:overflow-x-visible details-content:overflow-y-clip details-content:opacity-0 details-content:transition-[height,opacity,content-visibility] details-content:transition-discrete details-content:duration-200 details-content:ease-[cubic-bezier(0.23,1,0.32,1)] open:details-content:h-auto open:details-content:opacity-100 motion-reduce:details-content:transition-[opacity,content-visibility]";
|
|
211
|
+
/** Indent for a disclosure's content: the chevron's width plus the gap, so it lines up with the summary's text. */
|
|
212
|
+
const DISCLOSURE_CONTENT_INDENT = "ps-5.5";
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region src/blocks/heading.tsx
|
|
215
|
+
const LEVELS$1 = {
|
|
216
|
+
heading_1: {
|
|
217
|
+
Tag: "h2",
|
|
218
|
+
className: "mt-6 text-2xl leading-tight font-semibold tracking-tight"
|
|
219
|
+
},
|
|
220
|
+
heading_2: {
|
|
221
|
+
Tag: "h3",
|
|
222
|
+
className: "mt-4 text-xl leading-tight font-semibold tracking-tight"
|
|
223
|
+
},
|
|
224
|
+
heading_3: {
|
|
225
|
+
Tag: "h4",
|
|
226
|
+
className: "mt-4 text-lg leading-snug font-semibold tracking-tight"
|
|
227
|
+
},
|
|
228
|
+
heading_4: {
|
|
229
|
+
Tag: "h5",
|
|
230
|
+
className: "mt-4 text-base leading-snug font-semibold"
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
const ANCHOR = "relative ms-2 rounded-sm text-muted-foreground no-underline opacity-0 transition-opacity duration-150 ease-out group-hover:opacity-100 focus-visible:transition-none focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring pointer-coarse:text-[0.75em] pointer-coarse:font-normal pointer-coarse:opacity-100 after:absolute after:-inset-2 after:content-['']";
|
|
234
|
+
function Heading({ block, context, children }) {
|
|
235
|
+
const data = block.type === "heading_1" ? block.heading_1 : block.type === "heading_2" ? block.heading_2 : block.type === "heading_3" ? block.heading_3 : block.heading_4;
|
|
236
|
+
const text = plainText(data.rich_text);
|
|
237
|
+
const id = context.headings.find((heading) => heading.blockId === block.id)?.id;
|
|
238
|
+
if (!id) return children ?? null;
|
|
239
|
+
const { Tag, className } = LEVELS$1[block.type];
|
|
240
|
+
const heading = /* @__PURE__ */ jsx(Tag, {
|
|
241
|
+
id,
|
|
242
|
+
className: "inline scroll-mt-8",
|
|
243
|
+
children: /* @__PURE__ */ jsx(RichText, {
|
|
244
|
+
text: data.rich_text,
|
|
245
|
+
context
|
|
246
|
+
})
|
|
247
|
+
});
|
|
248
|
+
if (data.is_toggleable) return /* @__PURE__ */ jsxs("details", {
|
|
249
|
+
className: cx("group/toggle text-balance [hr+&]:mt-0", DISCLOSURE_CLASSES, className),
|
|
250
|
+
children: [/* @__PURE__ */ jsxs("summary", {
|
|
251
|
+
className: cx(SUMMARY_CLASSES, blockColorClass(data.color)),
|
|
252
|
+
children: [/* @__PURE__ */ jsx(DisclosureChevron, {}), heading]
|
|
253
|
+
}), children ? /* @__PURE__ */ jsx("div", {
|
|
254
|
+
className: cx("mt-2 flex flex-col gap-4 text-base leading-[1.6] font-normal tracking-normal text-wrap", DISCLOSURE_CONTENT_INDENT),
|
|
255
|
+
children
|
|
256
|
+
}) : null]
|
|
257
|
+
});
|
|
258
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
259
|
+
className: cx("group text-balance [hr+&]:mt-0", className, blockColorClass(data.color)),
|
|
260
|
+
children: [heading, /* @__PURE__ */ jsx("a", {
|
|
261
|
+
href: `#${id}`,
|
|
262
|
+
"aria-label": `Link to section: ${text}`,
|
|
263
|
+
className: ANCHOR,
|
|
264
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
265
|
+
"aria-hidden": "true",
|
|
266
|
+
children: "#"
|
|
267
|
+
})
|
|
268
|
+
})]
|
|
269
|
+
}), children] });
|
|
270
|
+
}
|
|
271
|
+
//#endregion
|
|
272
|
+
//#region src/blocks/layout.tsx
|
|
273
|
+
function Toggle({ block, context, children }) {
|
|
274
|
+
const { rich_text, color } = block.toggle;
|
|
275
|
+
return /* @__PURE__ */ jsxs("details", {
|
|
276
|
+
className: cx("group/toggle", DISCLOSURE_CLASSES, blockColorClass(color)),
|
|
277
|
+
children: [/* @__PURE__ */ jsxs("summary", {
|
|
278
|
+
className: SUMMARY_CLASSES,
|
|
279
|
+
children: [/* @__PURE__ */ jsx(DisclosureChevron, {}), /* @__PURE__ */ jsx("span", {
|
|
280
|
+
className: "min-w-0",
|
|
281
|
+
children: /* @__PURE__ */ jsx(RichText, {
|
|
282
|
+
text: rich_text,
|
|
283
|
+
context
|
|
284
|
+
})
|
|
285
|
+
})]
|
|
286
|
+
}), children ? /* @__PURE__ */ jsx("div", {
|
|
287
|
+
className: cx("mt-2 flex flex-col gap-4", DISCLOSURE_CONTENT_INDENT),
|
|
288
|
+
children
|
|
289
|
+
}) : null]
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Columns sit side by side once the space they're in is 36rem wide, whatever
|
|
294
|
+
* the screen: a renderer in a sidebar or card stacks them. The container is a
|
|
295
|
+
* wrapper inside the block flow, where its width is always defined; on the
|
|
296
|
+
* renderer's root, size containment would collapse it inside a flex row.
|
|
297
|
+
*/
|
|
298
|
+
function ColumnList({ children }) {
|
|
299
|
+
return /* @__PURE__ */ jsx("div", {
|
|
300
|
+
className: "@container",
|
|
301
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
302
|
+
className: "flex flex-col gap-4 @xl:flex-row @xl:gap-6",
|
|
303
|
+
children
|
|
304
|
+
})
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
function Column({ block, children }) {
|
|
308
|
+
const ratio = block.column.width_ratio;
|
|
309
|
+
return /* @__PURE__ */ jsx("div", {
|
|
310
|
+
className: cx("flex min-w-0 flex-col gap-4", !ratio && "@xl:flex-1"),
|
|
311
|
+
style: ratio ? { flex: `${ratio} 1 0%` } : void 0,
|
|
312
|
+
children
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/** Synced blocks: their content flows into the page. */
|
|
316
|
+
function SyncedBlock({ children }) {
|
|
317
|
+
return children ?? null;
|
|
318
|
+
}
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/blocks/list.tsx
|
|
321
|
+
function Nested({ children, className }) {
|
|
322
|
+
return children ? /* @__PURE__ */ jsx("div", {
|
|
323
|
+
className: cx("mt-2 flex flex-col gap-2", className),
|
|
324
|
+
children
|
|
325
|
+
}) : null;
|
|
326
|
+
}
|
|
327
|
+
function BulletedListItem({ block, context, children }) {
|
|
328
|
+
const { rich_text, color } = block.bulleted_list_item;
|
|
329
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
330
|
+
className: blockColorClass(color),
|
|
331
|
+
children: [/* @__PURE__ */ jsx(RichText, {
|
|
332
|
+
text: rich_text,
|
|
333
|
+
context
|
|
334
|
+
}), /* @__PURE__ */ jsx(Nested, { children })]
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
function NumberedListItem({ block, context, children }) {
|
|
338
|
+
const { rich_text, color } = block.numbered_list_item;
|
|
339
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
340
|
+
className: blockColorClass(color),
|
|
341
|
+
children: [/* @__PURE__ */ jsx(RichText, {
|
|
342
|
+
text: rich_text,
|
|
343
|
+
context
|
|
344
|
+
}), /* @__PURE__ */ jsx(Nested, { children })]
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
function ToDo({ block, context, children }) {
|
|
348
|
+
const { rich_text, checked, color } = block.to_do;
|
|
349
|
+
const text = /* @__PURE__ */ jsx(RichText, {
|
|
350
|
+
text: rich_text,
|
|
351
|
+
context
|
|
352
|
+
});
|
|
353
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
354
|
+
className: blockColorClass(color),
|
|
355
|
+
children: [/* @__PURE__ */ jsxs("label", {
|
|
356
|
+
className: "flex items-start gap-2",
|
|
357
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
358
|
+
className: "relative flex h-[1lh] shrink-0 items-center",
|
|
359
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
360
|
+
type: "checkbox",
|
|
361
|
+
checked,
|
|
362
|
+
disabled: true,
|
|
363
|
+
readOnly: true,
|
|
364
|
+
className: "peer size-4 appearance-none rounded-[4px] border border-muted-foreground/80 bg-background checked:border-primary checked:bg-primary"
|
|
365
|
+
}), /* @__PURE__ */ jsx("svg", {
|
|
366
|
+
"aria-hidden": "true",
|
|
367
|
+
viewBox: "0 0 24 24",
|
|
368
|
+
fill: "none",
|
|
369
|
+
stroke: "currentColor",
|
|
370
|
+
strokeWidth: "3",
|
|
371
|
+
strokeLinecap: "round",
|
|
372
|
+
strokeLinejoin: "round",
|
|
373
|
+
className: "pointer-events-none absolute inset-x-0.5 size-3 text-primary-foreground opacity-0 peer-checked:opacity-100",
|
|
374
|
+
children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" })
|
|
375
|
+
})]
|
|
376
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
377
|
+
className: cx(checked && "text-muted-foreground line-through"),
|
|
378
|
+
children: text
|
|
379
|
+
})]
|
|
380
|
+
}), /* @__PURE__ */ jsx(Nested, {
|
|
381
|
+
className: "ps-6",
|
|
382
|
+
children
|
|
383
|
+
})]
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
//#endregion
|
|
387
|
+
//#region src/demos.ts
|
|
388
|
+
/**
|
|
389
|
+
* The demo an embed URL names, or null. A URL names a demo when its path ends
|
|
390
|
+
* in `/demos/<name>` and `<name>` is one of `demos`. Only the path counts, so
|
|
391
|
+
* the same embed works on localhost, previews and production.
|
|
392
|
+
*/
|
|
393
|
+
function matchDemo(url, demos) {
|
|
394
|
+
if (!demos) return null;
|
|
395
|
+
let parsed;
|
|
396
|
+
let name;
|
|
397
|
+
try {
|
|
398
|
+
parsed = new URL(url);
|
|
399
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
400
|
+
if (segments.length < 2 || segments[segments.length - 2] !== "demos") return null;
|
|
401
|
+
name = decodeURIComponent(segments[segments.length - 1] ?? "");
|
|
402
|
+
} catch {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
if (!Object.hasOwn(demos, name)) return null;
|
|
406
|
+
const Component = demos[name];
|
|
407
|
+
if (!Component) return null;
|
|
408
|
+
return {
|
|
409
|
+
name,
|
|
410
|
+
params: Object.fromEntries(parsed.searchParams),
|
|
411
|
+
Component
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region src/blocks/icon.tsx
|
|
416
|
+
const BROKEN_IMAGE = "relative before:absolute before:inset-0 before:rounded-[inherit] before:bg-muted before:content-['']";
|
|
417
|
+
/**
|
|
418
|
+
* A Notion icon (an emoji, a custom emoji or an image) in a fixed slot one
|
|
419
|
+
* line tall, so it centers on the first line of the text beside it and the
|
|
420
|
+
* text starts at the same place whatever the icon. Without an icon, the
|
|
421
|
+
* fallback, or nothing.
|
|
422
|
+
*/
|
|
423
|
+
function IconSlot({ icon, blockId, context, fallback }) {
|
|
424
|
+
const url = icon?.type === "custom_emoji" ? icon.custom_emoji.url : icon?.type === "emoji" ? null : resolveFileUrl(icon, blockId, context);
|
|
425
|
+
const content = icon?.type === "emoji" ? icon.emoji : url ? /* @__PURE__ */ jsx("img", {
|
|
426
|
+
src: url,
|
|
427
|
+
alt: "",
|
|
428
|
+
className: cx("size-5 rounded-sm object-contain", BROKEN_IMAGE)
|
|
429
|
+
}) : fallback;
|
|
430
|
+
if (!content) return null;
|
|
431
|
+
return /* @__PURE__ */ jsx("span", {
|
|
432
|
+
"aria-hidden": "true",
|
|
433
|
+
className: "flex h-[1lh] w-5 shrink-0 items-center justify-center",
|
|
434
|
+
children: content
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
//#endregion
|
|
438
|
+
//#region src/blocks/media.tsx
|
|
439
|
+
/** An http(s) URL, or a same-site path such as a file proxy route. */
|
|
440
|
+
function isUrl(url) {
|
|
441
|
+
if (/^\/(?![/\\])/.test(url)) return true;
|
|
442
|
+
try {
|
|
443
|
+
const { protocol } = new URL(url);
|
|
444
|
+
return protocol === "http:" || protocol === "https:";
|
|
445
|
+
} catch {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function Caption({ caption, context }) {
|
|
450
|
+
if (caption.length === 0) return null;
|
|
451
|
+
return /* @__PURE__ */ jsx("figcaption", {
|
|
452
|
+
className: "mt-2 text-sm text-muted-foreground",
|
|
453
|
+
children: /* @__PURE__ */ jsx(RichText, {
|
|
454
|
+
text: caption,
|
|
455
|
+
context
|
|
456
|
+
})
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
/** A line linking to a URL, labelled by the caption or the URL itself. */
|
|
460
|
+
function UrlLine({ url, label, context }) {
|
|
461
|
+
const link = resolveLink(url, context);
|
|
462
|
+
if (!link) return null;
|
|
463
|
+
return /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx(NotionLink, {
|
|
464
|
+
link,
|
|
465
|
+
children: label || url
|
|
466
|
+
}) });
|
|
467
|
+
}
|
|
468
|
+
function Image({ block, context }) {
|
|
469
|
+
const src = resolveFileUrl(block.image, block.id, context);
|
|
470
|
+
if (!src || !isUrl(src)) return null;
|
|
471
|
+
return /* @__PURE__ */ jsxs("figure", { children: [/* @__PURE__ */ jsx("img", {
|
|
472
|
+
src,
|
|
473
|
+
alt: plainText(block.image.caption),
|
|
474
|
+
loading: "lazy",
|
|
475
|
+
decoding: "async",
|
|
476
|
+
className: cx("h-auto max-w-full", MEDIA_OUTLINE)
|
|
477
|
+
}), /* @__PURE__ */ jsx(Caption, {
|
|
478
|
+
caption: block.image.caption,
|
|
479
|
+
context
|
|
480
|
+
})] });
|
|
481
|
+
}
|
|
482
|
+
const MEDIA_OUTLINE = "rounded-lg outline outline-1 -outline-offset-1 outline-(--notionic-image-outline)";
|
|
483
|
+
const MEDIA_SURFACE = `${MEDIA_OUTLINE} bg-muted`;
|
|
484
|
+
const VIDEO_FILE = /\.(mp4|webm|mov|m4v|ogv)$/i;
|
|
485
|
+
function isVideoFile(url) {
|
|
486
|
+
try {
|
|
487
|
+
return VIDEO_FILE.test(new URL(url).pathname);
|
|
488
|
+
} catch {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
/** The embed URL for a YouTube link, or null. */
|
|
493
|
+
function youTubeEmbedUrl(url) {
|
|
494
|
+
let parsed;
|
|
495
|
+
try {
|
|
496
|
+
parsed = new URL(url);
|
|
497
|
+
} catch {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
const host = parsed.hostname.replace(/^(www\.|m\.)/, "");
|
|
501
|
+
let id = null;
|
|
502
|
+
if (host === "youtu.be") id = parsed.pathname.slice(1);
|
|
503
|
+
else if (host === "youtube.com" || host === "youtube-nocookie.com") id = parsed.searchParams.get("v") ?? parsed.pathname.match(/^\/(?:embed|shorts|live)\/([^/]+)/)?.[1] ?? null;
|
|
504
|
+
return id && /^[\w-]{6,}$/.test(id) ? `https://www.youtube-nocookie.com/embed/${id}` : null;
|
|
505
|
+
}
|
|
506
|
+
function Video({ block, context }) {
|
|
507
|
+
const src = resolveFileUrl(block.video, block.id, context) ?? "";
|
|
508
|
+
const caption = block.video.caption;
|
|
509
|
+
const label = plainText(caption);
|
|
510
|
+
const embed = youTubeEmbedUrl(src);
|
|
511
|
+
if (embed) return /* @__PURE__ */ jsxs("figure", { children: [/* @__PURE__ */ jsx("iframe", {
|
|
512
|
+
src: embed,
|
|
513
|
+
title: label || "YouTube video",
|
|
514
|
+
allow: "accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",
|
|
515
|
+
referrerPolicy: "strict-origin-when-cross-origin",
|
|
516
|
+
allowFullScreen: true,
|
|
517
|
+
loading: "lazy",
|
|
518
|
+
className: cx("aspect-video w-full", MEDIA_SURFACE)
|
|
519
|
+
}), /* @__PURE__ */ jsx(Caption, {
|
|
520
|
+
caption,
|
|
521
|
+
context
|
|
522
|
+
})] });
|
|
523
|
+
if (isUrl(src) && (block.video.type === "file" || isVideoFile(src))) return /* @__PURE__ */ jsxs("figure", { children: [/* @__PURE__ */ jsx("video", {
|
|
524
|
+
src,
|
|
525
|
+
controls: true,
|
|
526
|
+
preload: "metadata",
|
|
527
|
+
"aria-label": label || "Video",
|
|
528
|
+
className: cx("w-full", MEDIA_SURFACE)
|
|
529
|
+
}), /* @__PURE__ */ jsx(Caption, {
|
|
530
|
+
caption,
|
|
531
|
+
context
|
|
532
|
+
})] });
|
|
533
|
+
return /* @__PURE__ */ jsx(UrlLine, {
|
|
534
|
+
url: src,
|
|
535
|
+
label,
|
|
536
|
+
context
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
function Audio({ block, context }) {
|
|
540
|
+
const src = resolveFileUrl(block.audio, block.id, context);
|
|
541
|
+
if (!src || !isUrl(src)) return null;
|
|
542
|
+
return /* @__PURE__ */ jsxs("figure", { children: [/* @__PURE__ */ jsx("audio", {
|
|
543
|
+
src,
|
|
544
|
+
controls: true,
|
|
545
|
+
preload: "metadata",
|
|
546
|
+
"aria-label": plainText(block.audio.caption) || "Audio",
|
|
547
|
+
className: "w-full"
|
|
548
|
+
}), /* @__PURE__ */ jsx(Caption, {
|
|
549
|
+
caption: block.audio.caption,
|
|
550
|
+
context
|
|
551
|
+
})] });
|
|
552
|
+
}
|
|
553
|
+
/** The extension of a file name or URL path, upper-cased ("report.pdf" is "PDF"), or null. */
|
|
554
|
+
function fileExtension(name) {
|
|
555
|
+
const match = name.match(/\.([a-z0-9]{1,8})$/i);
|
|
556
|
+
return match?.[1] ? match[1].toUpperCase() : null;
|
|
557
|
+
}
|
|
558
|
+
/** The last segment of a URL's path, decoded: "…/files/report.pdf?X=1" is "report.pdf". */
|
|
559
|
+
function fileNameFromUrl(url) {
|
|
560
|
+
try {
|
|
561
|
+
return decodeURIComponent(new URL(url, "https://notionic.dev").pathname.split("/").pop() ?? "");
|
|
562
|
+
} catch {
|
|
563
|
+
return "";
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/** The URL Notion gave for a file, before `transformFileUrl`. */
|
|
567
|
+
function sourceUrl(file) {
|
|
568
|
+
return "external" in file ? file.external.url : file.file.url;
|
|
569
|
+
}
|
|
570
|
+
/** A URL without its protocol, `www.` or a trailing slash: "github.com/vercel/next.js". */
|
|
571
|
+
function displayUrl(url) {
|
|
572
|
+
return url.replace(/^https?:\/\/(www\.)?/, "").replace(/\/$/, "");
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* A link as a one-line bordered row: an icon, a title that truncates (the full
|
|
576
|
+
* title as a tooltip), an optional detail at the end, and the caption below.
|
|
577
|
+
*/
|
|
578
|
+
function CardRow({ url, title, icon, detail, caption, context }) {
|
|
579
|
+
const link = resolveLink(url, context);
|
|
580
|
+
if (!link) return null;
|
|
581
|
+
return /* @__PURE__ */ jsxs("figure", { children: [/* @__PURE__ */ jsxs("a", {
|
|
582
|
+
href: link.href,
|
|
583
|
+
title,
|
|
584
|
+
...link.external ? {
|
|
585
|
+
target: "_blank",
|
|
586
|
+
rel: "noopener noreferrer"
|
|
587
|
+
} : {},
|
|
588
|
+
className: "flex items-center gap-3 rounded-lg border border-border px-3 py-2.5 transition-[background-color,scale] hover:bg-muted/50 active:scale-[0.99] motion-reduce:active:scale-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
|
|
589
|
+
children: [
|
|
590
|
+
/* @__PURE__ */ jsx("svg", {
|
|
591
|
+
"aria-hidden": "true",
|
|
592
|
+
viewBox: "0 0 24 24",
|
|
593
|
+
fill: "none",
|
|
594
|
+
stroke: "currentColor",
|
|
595
|
+
strokeWidth: "1.5",
|
|
596
|
+
strokeLinecap: "round",
|
|
597
|
+
strokeLinejoin: "round",
|
|
598
|
+
className: "size-5 shrink-0 text-muted-foreground",
|
|
599
|
+
children: icon
|
|
600
|
+
}),
|
|
601
|
+
/* @__PURE__ */ jsx("span", {
|
|
602
|
+
className: "min-w-0 flex-1 truncate font-medium",
|
|
603
|
+
children: title
|
|
604
|
+
}),
|
|
605
|
+
detail ? /* @__PURE__ */ jsx("span", {
|
|
606
|
+
className: "shrink-0 text-xs text-muted-foreground",
|
|
607
|
+
children: detail
|
|
608
|
+
}) : null,
|
|
609
|
+
link.external ? /* @__PURE__ */ jsx("span", {
|
|
610
|
+
className: "sr-only",
|
|
611
|
+
children: " (opens in a new tab)"
|
|
612
|
+
}) : null
|
|
613
|
+
]
|
|
614
|
+
}), /* @__PURE__ */ jsx(Caption, {
|
|
615
|
+
caption,
|
|
616
|
+
context
|
|
617
|
+
})] });
|
|
618
|
+
}
|
|
619
|
+
const FILE_ICON = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" }), /* @__PURE__ */ jsx("path", { d: "M14 2v4a2 2 0 0 0 2 2h4" })] });
|
|
620
|
+
const LINK_ICON = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }), /* @__PURE__ */ jsx("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })] });
|
|
621
|
+
const EMBED_ICON = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
622
|
+
/* @__PURE__ */ jsx("rect", {
|
|
623
|
+
width: "20",
|
|
624
|
+
height: "16",
|
|
625
|
+
x: "2",
|
|
626
|
+
y: "4",
|
|
627
|
+
rx: "2"
|
|
628
|
+
}),
|
|
629
|
+
/* @__PURE__ */ jsx("path", { d: "M10 4v4" }),
|
|
630
|
+
/* @__PURE__ */ jsx("path", { d: "M2 8h20" }),
|
|
631
|
+
/* @__PURE__ */ jsx("path", { d: "M6 4v4" })
|
|
632
|
+
] });
|
|
633
|
+
/** A file as a card: its name, its type ("PDF"), and the caption below. */
|
|
634
|
+
function FileCard({ url, name, caption, context }) {
|
|
635
|
+
return /* @__PURE__ */ jsx(CardRow, {
|
|
636
|
+
url,
|
|
637
|
+
title: name || url,
|
|
638
|
+
icon: FILE_ICON,
|
|
639
|
+
detail: fileExtension(name),
|
|
640
|
+
caption,
|
|
641
|
+
context
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
function File({ block, context }) {
|
|
645
|
+
return /* @__PURE__ */ jsx(FileCard, {
|
|
646
|
+
url: resolveFileUrl(block.file, block.id, context) ?? "",
|
|
647
|
+
name: block.file.name || fileNameFromUrl(sourceUrl(block.file)),
|
|
648
|
+
caption: block.file.caption,
|
|
649
|
+
context
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
function Pdf({ block, context }) {
|
|
653
|
+
return /* @__PURE__ */ jsx(FileCard, {
|
|
654
|
+
url: resolveFileUrl(block.pdf, block.id, context) ?? "",
|
|
655
|
+
name: fileNameFromUrl(sourceUrl(block.pdf)),
|
|
656
|
+
caption: block.pdf.caption,
|
|
657
|
+
context
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
function Bookmark({ block, context }) {
|
|
661
|
+
const { url, caption } = block.bookmark;
|
|
662
|
+
return /* @__PURE__ */ jsx(PreviewCard, {
|
|
663
|
+
url,
|
|
664
|
+
preview: block.bookmarkPreview,
|
|
665
|
+
icon: LINK_ICON,
|
|
666
|
+
caption,
|
|
667
|
+
context
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* A link with the linked page's title, description and image, as in Notion's
|
|
672
|
+
* bookmarks: the image sits on the right, or on top when the card is narrow (a
|
|
673
|
+
* phone, a column). Without a preview from `fetchBlocks` (none fetched, or the
|
|
674
|
+
* page had no title), a one-line card.
|
|
675
|
+
*/
|
|
676
|
+
function PreviewCard({ url, preview, icon, caption, context }) {
|
|
677
|
+
const link = resolveLink(url, context);
|
|
678
|
+
if (!preview || !link) return /* @__PURE__ */ jsx(CardRow, {
|
|
679
|
+
url,
|
|
680
|
+
title: displayUrl(url),
|
|
681
|
+
icon,
|
|
682
|
+
caption,
|
|
683
|
+
context
|
|
684
|
+
});
|
|
685
|
+
return /* @__PURE__ */ jsxs("figure", {
|
|
686
|
+
className: "@container",
|
|
687
|
+
children: [/* @__PURE__ */ jsxs("a", {
|
|
688
|
+
href: link.href,
|
|
689
|
+
...link.external ? {
|
|
690
|
+
target: "_blank",
|
|
691
|
+
rel: "noopener noreferrer"
|
|
692
|
+
} : {},
|
|
693
|
+
className: "flex flex-col-reverse overflow-hidden rounded-lg border border-border transition-[background-color,scale] @md:min-h-26 @md:flex-row hover:bg-muted/50 active:scale-[0.99] motion-reduce:active:scale-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
|
|
694
|
+
children: [
|
|
695
|
+
/* @__PURE__ */ jsxs("span", {
|
|
696
|
+
className: "flex min-w-0 flex-1 flex-col gap-1 p-3.5",
|
|
697
|
+
children: [
|
|
698
|
+
/* @__PURE__ */ jsx("span", {
|
|
699
|
+
className: "line-clamp-2 text-sm font-medium @md:line-clamp-1",
|
|
700
|
+
children: preview.title
|
|
701
|
+
}),
|
|
702
|
+
preview.description ? /* @__PURE__ */ jsx("span", {
|
|
703
|
+
className: "line-clamp-2 text-xs text-muted-foreground",
|
|
704
|
+
children: preview.description
|
|
705
|
+
}) : null,
|
|
706
|
+
/* @__PURE__ */ jsxs("span", {
|
|
707
|
+
className: "mt-auto flex min-w-0 items-center gap-1.5 pt-1.5 text-xs text-muted-foreground",
|
|
708
|
+
children: [preview.favicon ? /* @__PURE__ */ jsx("img", {
|
|
709
|
+
src: preview.favicon,
|
|
710
|
+
alt: "",
|
|
711
|
+
width: 16,
|
|
712
|
+
height: 16,
|
|
713
|
+
loading: "lazy",
|
|
714
|
+
referrerPolicy: "no-referrer",
|
|
715
|
+
className: cx("size-4 shrink-0 rounded-sm", BROKEN_IMAGE)
|
|
716
|
+
}) : null, /* @__PURE__ */ jsx("span", {
|
|
717
|
+
className: "truncate",
|
|
718
|
+
children: displayUrl(url)
|
|
719
|
+
})]
|
|
720
|
+
})
|
|
721
|
+
]
|
|
722
|
+
}),
|
|
723
|
+
preview.image ? /* @__PURE__ */ jsx("img", {
|
|
724
|
+
src: preview.image,
|
|
725
|
+
alt: "",
|
|
726
|
+
loading: "lazy",
|
|
727
|
+
decoding: "async",
|
|
728
|
+
referrerPolicy: "no-referrer",
|
|
729
|
+
className: cx("aspect-[1.91/1] w-full border-b border-border bg-muted object-cover @md:aspect-auto @md:w-1/3 @md:max-w-60 @md:shrink-0 @md:border-b-0 @md:border-s", BROKEN_IMAGE)
|
|
730
|
+
}) : null,
|
|
731
|
+
link.external ? /* @__PURE__ */ jsx("span", {
|
|
732
|
+
className: "sr-only",
|
|
733
|
+
children: " (opens in a new tab)"
|
|
734
|
+
}) : null
|
|
735
|
+
]
|
|
736
|
+
}), /* @__PURE__ */ jsx(Caption, {
|
|
737
|
+
caption,
|
|
738
|
+
context
|
|
739
|
+
})]
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
/** The default frame around a demo: a bordered stage with the caption below. */
|
|
743
|
+
function DemoFrame({ caption, context, children }) {
|
|
744
|
+
return /* @__PURE__ */ jsxs("figure", { children: [/* @__PURE__ */ jsx("div", {
|
|
745
|
+
className: "flex min-h-48 flex-col items-center justify-center gap-6 overflow-hidden rounded-xl border border-border px-6 py-10",
|
|
746
|
+
children
|
|
747
|
+
}), caption.length ? /* @__PURE__ */ jsx("figcaption", {
|
|
748
|
+
className: "mt-3 text-center text-sm text-muted-foreground",
|
|
749
|
+
children: /* @__PURE__ */ jsx(RichText, {
|
|
750
|
+
text: caption,
|
|
751
|
+
context
|
|
752
|
+
})
|
|
753
|
+
}) : null] });
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* An embed, as a preview card linking to it: embedding any site in an iframe
|
|
757
|
+
* is unsafe, and most refuse it. An embed of `…/demos/<name>` whose name is one of
|
|
758
|
+
* `demos` renders that demo component instead, in the `demo` frame.
|
|
759
|
+
*/
|
|
760
|
+
function Embed({ block, context }) {
|
|
761
|
+
const demo = matchDemo(block.embed.url, context.demos);
|
|
762
|
+
if (demo) {
|
|
763
|
+
const Frame = context.components.demo ?? DemoFrame;
|
|
764
|
+
return /* @__PURE__ */ jsx(Frame, {
|
|
765
|
+
name: demo.name,
|
|
766
|
+
caption: block.embed.caption,
|
|
767
|
+
context,
|
|
768
|
+
children: /* @__PURE__ */ jsx(demo.Component, {
|
|
769
|
+
name: demo.name,
|
|
770
|
+
params: demo.params
|
|
771
|
+
})
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
return /* @__PURE__ */ jsx(PreviewCard, {
|
|
775
|
+
url: block.embed.url,
|
|
776
|
+
preview: block.bookmarkPreview,
|
|
777
|
+
icon: EMBED_ICON,
|
|
778
|
+
caption: block.embed.caption,
|
|
779
|
+
context
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
function LinkPreview({ block, context }) {
|
|
783
|
+
return /* @__PURE__ */ jsx(PreviewCard, {
|
|
784
|
+
url: block.link_preview.url,
|
|
785
|
+
preview: block.bookmarkPreview,
|
|
786
|
+
icon: LINK_ICON,
|
|
787
|
+
caption: [],
|
|
788
|
+
context
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
const PAGE_ICON = /* @__PURE__ */ jsx("svg", {
|
|
792
|
+
"aria-hidden": "true",
|
|
793
|
+
viewBox: "0 0 24 24",
|
|
794
|
+
fill: "none",
|
|
795
|
+
stroke: "currentColor",
|
|
796
|
+
strokeWidth: "1.5",
|
|
797
|
+
strokeLinecap: "round",
|
|
798
|
+
strokeLinejoin: "round",
|
|
799
|
+
className: "size-5 text-muted-foreground",
|
|
800
|
+
children: FILE_ICON
|
|
801
|
+
});
|
|
802
|
+
/**
|
|
803
|
+
* A page as a row: its icon (or a page icon), then its title. Rows that
|
|
804
|
+
* follow each other sit close together, like a list of subpages in Notion.
|
|
805
|
+
*/
|
|
806
|
+
function PageRow({ pageId, title, icon, blockId, context }) {
|
|
807
|
+
const link = resolvePageLink(pageId, context);
|
|
808
|
+
const row = "flex flex-1 gap-2 rounded-md px-2 py-1";
|
|
809
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(IconSlot, {
|
|
810
|
+
icon,
|
|
811
|
+
blockId,
|
|
812
|
+
context,
|
|
813
|
+
fallback: PAGE_ICON
|
|
814
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
815
|
+
className: "min-w-0 font-medium underline decoration-muted-foreground/50 decoration-from-font underline-offset-4 transition-[text-decoration-color] group-hover:decoration-foreground",
|
|
816
|
+
children: title || "Untitled"
|
|
817
|
+
})] });
|
|
818
|
+
return /* @__PURE__ */ jsx("p", {
|
|
819
|
+
"data-page-row": "",
|
|
820
|
+
className: "-mx-2 flex [[data-page-row]+&]:-mt-3",
|
|
821
|
+
children: link ? /* @__PURE__ */ jsxs("a", {
|
|
822
|
+
href: link.href,
|
|
823
|
+
...link.external ? {
|
|
824
|
+
target: "_blank",
|
|
825
|
+
rel: "noopener noreferrer"
|
|
826
|
+
} : {},
|
|
827
|
+
className: cx("group transition-[background-color] hover:bg-muted/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", row),
|
|
828
|
+
children: [content, link.external ? /* @__PURE__ */ jsx("span", {
|
|
829
|
+
className: "sr-only",
|
|
830
|
+
children: " (opens in a new tab)"
|
|
831
|
+
}) : null]
|
|
832
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
833
|
+
className: row,
|
|
834
|
+
children: content
|
|
835
|
+
})
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
/** A subpage, with its icon when `fetchBlocks` attached it. */
|
|
839
|
+
function ChildPage({ block, context }) {
|
|
840
|
+
return /* @__PURE__ */ jsx(PageRow, {
|
|
841
|
+
pageId: block.id,
|
|
842
|
+
title: block.child_page.title,
|
|
843
|
+
icon: block.linkedPage?.icon,
|
|
844
|
+
blockId: block.id,
|
|
845
|
+
context
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
/** A link to another page, titled by `fetchBlocks`. Without a title, nothing renders. */
|
|
849
|
+
function LinkToPage({ block, context }) {
|
|
850
|
+
const page = block.linkedPage;
|
|
851
|
+
if (!page) return null;
|
|
852
|
+
return /* @__PURE__ */ jsx(PageRow, {
|
|
853
|
+
pageId: page.id,
|
|
854
|
+
title: page.title,
|
|
855
|
+
icon: page.icon,
|
|
856
|
+
blockId: block.id,
|
|
857
|
+
context
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
//#endregion
|
|
861
|
+
//#region src/blocks/table.tsx
|
|
862
|
+
const CELL = "max-w-64 border border-border px-4 py-2 text-start align-top";
|
|
863
|
+
const HEADER_CELL = `${CELL} bg-muted font-semibold`;
|
|
864
|
+
function cells(row) {
|
|
865
|
+
return row.type === "table_row" ? row.table_row.cells : [];
|
|
866
|
+
}
|
|
867
|
+
/** Renders its rows itself, from `block.children`. */
|
|
868
|
+
function Table({ block, context }) {
|
|
869
|
+
const { has_column_header, has_row_header } = block.table;
|
|
870
|
+
const rows = (block.children ?? []).filter((row) => row.type === "table_row");
|
|
871
|
+
const [first, ...rest] = rows;
|
|
872
|
+
if (!first) return null;
|
|
873
|
+
const renderRow = (row, head) => /* @__PURE__ */ jsx("tr", { children: cells(row).map((cell, i) => head ? /* @__PURE__ */ jsx("th", {
|
|
874
|
+
scope: "col",
|
|
875
|
+
className: HEADER_CELL,
|
|
876
|
+
children: /* @__PURE__ */ jsx(RichText, {
|
|
877
|
+
text: cell,
|
|
878
|
+
context
|
|
879
|
+
})
|
|
880
|
+
}, i) : i === 0 && has_row_header ? /* @__PURE__ */ jsx("th", {
|
|
881
|
+
scope: "row",
|
|
882
|
+
className: HEADER_CELL,
|
|
883
|
+
children: /* @__PURE__ */ jsx(RichText, {
|
|
884
|
+
text: cell,
|
|
885
|
+
context
|
|
886
|
+
})
|
|
887
|
+
}, i) : /* @__PURE__ */ jsx("td", {
|
|
888
|
+
className: CELL,
|
|
889
|
+
children: /* @__PURE__ */ jsx(RichText, {
|
|
890
|
+
text: cell,
|
|
891
|
+
context
|
|
892
|
+
})
|
|
893
|
+
}, i)) }, row.id);
|
|
894
|
+
const columns = has_column_header ? cells(first).map((cell) => plainText(cell).trim()).filter(Boolean) : [];
|
|
895
|
+
return /* @__PURE__ */ jsx("div", {
|
|
896
|
+
role: "region",
|
|
897
|
+
"aria-label": columns.length > 0 ? `Table: ${columns.join(", ")}` : "Table",
|
|
898
|
+
tabIndex: 0,
|
|
899
|
+
className: "overflow-x-auto rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
|
|
900
|
+
children: /* @__PURE__ */ jsx("table", {
|
|
901
|
+
className: "w-max min-w-full border-collapse text-sm tabular-nums",
|
|
902
|
+
children: has_column_header ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("thead", { children: renderRow(first, true) }), /* @__PURE__ */ jsx("tbody", { children: rest.map((row) => renderRow(row, false)) })] }) : /* @__PURE__ */ jsx("tbody", { children: rows.map((row) => renderRow(row, false)) })
|
|
903
|
+
})
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
//#endregion
|
|
907
|
+
//#region src/blocks/navigation.tsx
|
|
908
|
+
const INDENT = [
|
|
909
|
+
"",
|
|
910
|
+
"ps-4",
|
|
911
|
+
"ps-8",
|
|
912
|
+
"ps-12"
|
|
913
|
+
];
|
|
914
|
+
/** Links to every heading on the page, indented by level. */
|
|
915
|
+
function TableOfContents({ block, context }) {
|
|
916
|
+
const { headings } = context;
|
|
917
|
+
if (headings.length === 0) return null;
|
|
918
|
+
const top = Math.min(...headings.map((heading) => heading.level));
|
|
919
|
+
return /* @__PURE__ */ jsx("nav", {
|
|
920
|
+
"aria-label": "Table of contents",
|
|
921
|
+
className: cx("text-sm", blockColorClass(block.table_of_contents.color)),
|
|
922
|
+
children: /* @__PURE__ */ jsx("ul", {
|
|
923
|
+
className: "flex flex-col",
|
|
924
|
+
children: headings.map((heading) => /* @__PURE__ */ jsx("li", {
|
|
925
|
+
className: INDENT[heading.level - top],
|
|
926
|
+
children: /* @__PURE__ */ jsx("a", {
|
|
927
|
+
href: `#${heading.id}`,
|
|
928
|
+
className: "block rounded-sm py-1 text-muted-foreground underline decoration-transparent underline-offset-4 transition-[color,text-decoration-color] duration-150 hover:text-foreground hover:decoration-current focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
|
|
929
|
+
children: heading.text
|
|
930
|
+
})
|
|
931
|
+
}, heading.blockId))
|
|
932
|
+
})
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
const SHOW_PANEL = [
|
|
936
|
+
"[&:has([data-tab='0']:checked)_[data-panel='0']]:flex",
|
|
937
|
+
"[&:has([data-tab='1']:checked)_[data-panel='1']]:flex",
|
|
938
|
+
"[&:has([data-tab='2']:checked)_[data-panel='2']]:flex",
|
|
939
|
+
"[&:has([data-tab='3']:checked)_[data-panel='3']]:flex",
|
|
940
|
+
"[&:has([data-tab='4']:checked)_[data-panel='4']]:flex",
|
|
941
|
+
"[&:has([data-tab='5']:checked)_[data-panel='5']]:flex",
|
|
942
|
+
"[&:has([data-tab='6']:checked)_[data-panel='6']]:flex",
|
|
943
|
+
"[&:has([data-tab='7']:checked)_[data-panel='7']]:flex",
|
|
944
|
+
"[&:has([data-tab='8']:checked)_[data-panel='8']]:flex",
|
|
945
|
+
"[&:has([data-tab='9']:checked)_[data-panel='9']]:flex",
|
|
946
|
+
"[&:has([data-tab='10']:checked)_[data-panel='10']]:flex",
|
|
947
|
+
"[&:has([data-tab='11']:checked)_[data-panel='11']]:flex",
|
|
948
|
+
"[&:has([data-tab='12']:checked)_[data-panel='12']]:flex",
|
|
949
|
+
"[&:has([data-tab='13']:checked)_[data-panel='13']]:flex",
|
|
950
|
+
"[&:has([data-tab='14']:checked)_[data-panel='14']]:flex",
|
|
951
|
+
"[&:has([data-tab='15']:checked)_[data-panel='15']]:flex",
|
|
952
|
+
"[&:has([data-tab='16']:checked)_[data-panel='16']]:flex",
|
|
953
|
+
"[&:has([data-tab='17']:checked)_[data-panel='17']]:flex",
|
|
954
|
+
"[&:has([data-tab='18']:checked)_[data-panel='18']]:flex",
|
|
955
|
+
"[&:has([data-tab='19']:checked)_[data-panel='19']]:flex"
|
|
956
|
+
];
|
|
957
|
+
/**
|
|
958
|
+
* Notion tabs, with no JavaScript: each tab is a radio button drawn as a pill,
|
|
959
|
+
* in a row that scrolls sideways when the tabs don't fit, and CSS shows the
|
|
960
|
+
* panel of the checked one. The first tab starts selected. Radios give
|
|
961
|
+
* keyboard support for free (arrow keys switch tabs), and screen readers
|
|
962
|
+
* announce "pnpm, radio button, 2 of 3". Each tab is a paragraph (its title
|
|
963
|
+
* and icon) whose children are the tab's content.
|
|
964
|
+
*/
|
|
965
|
+
function Tab({ block, context }) {
|
|
966
|
+
const tabs = (block.children ?? []).filter((child) => child.type === "paragraph").slice(0, SHOW_PANEL.length);
|
|
967
|
+
if (tabs.length === 0) return null;
|
|
968
|
+
const name = `notionic-tabs-${block.id}`;
|
|
969
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
970
|
+
className: cx("group/tabs rounded-xl border border-border p-2", ...SHOW_PANEL.slice(0, tabs.length)),
|
|
971
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
972
|
+
className: "flex gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
|
|
973
|
+
children: tabs.map((tab, index) => {
|
|
974
|
+
if (tab.type !== "paragraph") return null;
|
|
975
|
+
const { rich_text, icon } = tab.paragraph;
|
|
976
|
+
const id = `${name}-${index}`;
|
|
977
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
978
|
+
className: "relative flex shrink-0",
|
|
979
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
980
|
+
type: "radio",
|
|
981
|
+
name,
|
|
982
|
+
id,
|
|
983
|
+
"data-tab": index,
|
|
984
|
+
defaultChecked: index === 0,
|
|
985
|
+
className: "pointer-events-none absolute inset-0 appearance-none opacity-0"
|
|
986
|
+
}), /* @__PURE__ */ jsxs("label", {
|
|
987
|
+
htmlFor: id,
|
|
988
|
+
className: "flex cursor-pointer items-center gap-2 rounded-lg px-3 py-1.5 font-medium whitespace-nowrap text-muted-foreground transition-[color,background-color] duration-150 hover:text-foreground motion-reduce:transition-none [:checked+&]:bg-muted [:checked+&]:text-foreground [:focus-visible+&]:outline-2 [:focus-visible+&]:-outline-offset-2 [:focus-visible+&]:outline-ring",
|
|
989
|
+
children: [icon?.type === "emoji" ? /* @__PURE__ */ jsx("span", {
|
|
990
|
+
"aria-hidden": "true",
|
|
991
|
+
children: icon.emoji
|
|
992
|
+
}) : null, /* @__PURE__ */ jsx(RichText, {
|
|
993
|
+
text: rich_text,
|
|
994
|
+
context
|
|
995
|
+
})]
|
|
996
|
+
})]
|
|
997
|
+
}, tab.id);
|
|
998
|
+
})
|
|
999
|
+
}), tabs.map((tab, index) => /* @__PURE__ */ jsx("div", {
|
|
1000
|
+
"data-panel": index,
|
|
1001
|
+
className: "hidden flex-col gap-4 px-3 pt-3 pb-2 transition-opacity duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] group-focus-within/tabs:starting:opacity-0",
|
|
1002
|
+
children: /* @__PURE__ */ jsx(BlockList, {
|
|
1003
|
+
blocks: tab.children,
|
|
1004
|
+
context
|
|
1005
|
+
})
|
|
1006
|
+
}, tab.id))]
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
//#endregion
|
|
1010
|
+
//#region src/blocks/text.tsx
|
|
1011
|
+
function Paragraph({ block, context, children }) {
|
|
1012
|
+
const { rich_text, color } = block.paragraph;
|
|
1013
|
+
const empty = rich_text.length === 0;
|
|
1014
|
+
if (empty && !children) return null;
|
|
1015
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [empty ? null : /* @__PURE__ */ jsx("p", {
|
|
1016
|
+
className: blockColorClass(color),
|
|
1017
|
+
children: /* @__PURE__ */ jsx(RichText, {
|
|
1018
|
+
text: rich_text,
|
|
1019
|
+
context
|
|
1020
|
+
})
|
|
1021
|
+
}), children ? /* @__PURE__ */ jsx("div", {
|
|
1022
|
+
className: "flex flex-col gap-4 ps-6",
|
|
1023
|
+
children
|
|
1024
|
+
}) : null] });
|
|
1025
|
+
}
|
|
1026
|
+
function Quote({ block, context, children }) {
|
|
1027
|
+
const { rich_text, color } = block.quote;
|
|
1028
|
+
return /* @__PURE__ */ jsxs("blockquote", {
|
|
1029
|
+
className: cx("border-s-2 border-border ps-4 italic [&_em]:not-italic", colorClass(color), isBackgroundColor(color) && "rounded-e-md py-1 pe-3"),
|
|
1030
|
+
children: [/* @__PURE__ */ jsx(RichText, {
|
|
1031
|
+
text: rich_text,
|
|
1032
|
+
context
|
|
1033
|
+
}), children ? /* @__PURE__ */ jsx("div", {
|
|
1034
|
+
className: "mt-4 flex flex-col gap-4",
|
|
1035
|
+
children
|
|
1036
|
+
}) : null]
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
function Callout({ block, context, children }) {
|
|
1040
|
+
const { icon, rich_text, color } = block.callout;
|
|
1041
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1042
|
+
className: cx("flex gap-3 rounded-lg p-4", isBackgroundColor(color) ? cx("border border-transparent", colorClass(color)) : cx("border border-border bg-muted/50", colorClass(color))),
|
|
1043
|
+
children: [/* @__PURE__ */ jsx(IconSlot, {
|
|
1044
|
+
icon,
|
|
1045
|
+
blockId: block.id,
|
|
1046
|
+
context
|
|
1047
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1048
|
+
className: "flex min-w-0 flex-1 flex-col gap-4",
|
|
1049
|
+
children: [rich_text.length > 0 ? /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx(RichText, {
|
|
1050
|
+
text: rich_text,
|
|
1051
|
+
context
|
|
1052
|
+
}) }) : null, children]
|
|
1053
|
+
})]
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
function Divider(_) {
|
|
1057
|
+
return /* @__PURE__ */ jsx("hr", { className: "my-4 border-border" });
|
|
1058
|
+
}
|
|
1059
|
+
/** An equation's LaTeX source. For formatted math, use `notion-equation` from the shadcn registry. */
|
|
1060
|
+
function Equation({ block }) {
|
|
1061
|
+
return /* @__PURE__ */ jsx("figure", {
|
|
1062
|
+
"aria-label": "Equation, LaTeX source",
|
|
1063
|
+
className: "rounded-lg border border-border bg-muted px-4 py-3 font-mono text-[0.8125rem] leading-relaxed",
|
|
1064
|
+
children: /* @__PURE__ */ jsx("code", { children: block.equation.expression })
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
/** An equation inside text, as its LaTeX source. */
|
|
1068
|
+
function InlineEquation({ expression }) {
|
|
1069
|
+
return /* @__PURE__ */ jsx("code", {
|
|
1070
|
+
className: CODE_CLASSES,
|
|
1071
|
+
children: expression
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
//#endregion
|
|
1075
|
+
//#region src/slug.ts
|
|
1076
|
+
/** Lowercase, accent-free, hyphenated anchor id for a heading's text. */
|
|
1077
|
+
function slugify(text) {
|
|
1078
|
+
return text.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "section";
|
|
1079
|
+
}
|
|
1080
|
+
/** Slugs for one page: the first use is plain, repeats get -2, -3… */
|
|
1081
|
+
function createSlugger() {
|
|
1082
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1083
|
+
return (text) => {
|
|
1084
|
+
const slug = slugify(text);
|
|
1085
|
+
const count = (seen.get(slug) ?? 0) + 1;
|
|
1086
|
+
seen.set(slug, count);
|
|
1087
|
+
return count === 1 ? slug : `${slug}-${count}`;
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
//#endregion
|
|
1091
|
+
//#region src/headings.ts
|
|
1092
|
+
const LEVELS = {
|
|
1093
|
+
heading_1: 1,
|
|
1094
|
+
heading_2: 2,
|
|
1095
|
+
heading_3: 3,
|
|
1096
|
+
heading_4: 4
|
|
1097
|
+
};
|
|
1098
|
+
/**
|
|
1099
|
+
* Every non-blank heading on the page, in reading order (headings nested in
|
|
1100
|
+
* toggles, columns and callouts included), with unique anchor ids. Use it for
|
|
1101
|
+
* a table of contents or a sidebar outline.
|
|
1102
|
+
*/
|
|
1103
|
+
function collectHeadings(blocks) {
|
|
1104
|
+
const slug = createSlugger();
|
|
1105
|
+
const headings = [];
|
|
1106
|
+
const walk = (list) => {
|
|
1107
|
+
for (const block of list) {
|
|
1108
|
+
if (block.type === "heading_1" || block.type === "heading_2" || block.type === "heading_3" || block.type === "heading_4") {
|
|
1109
|
+
const data = block.type === "heading_1" ? block.heading_1 : block.type === "heading_2" ? block.heading_2 : block.type === "heading_3" ? block.heading_3 : block.heading_4;
|
|
1110
|
+
const text = plainText(data.rich_text).trim();
|
|
1111
|
+
if (text) headings.push({
|
|
1112
|
+
blockId: block.id,
|
|
1113
|
+
id: slug(text),
|
|
1114
|
+
text,
|
|
1115
|
+
level: LEVELS[block.type]
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
if (block.children) walk(block.children);
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
walk(blocks);
|
|
1122
|
+
return headings;
|
|
1123
|
+
}
|
|
1124
|
+
//#endregion
|
|
1125
|
+
//#region src/renderer.tsx
|
|
1126
|
+
const defaultComponents = {
|
|
1127
|
+
paragraph: Paragraph,
|
|
1128
|
+
heading_1: Heading,
|
|
1129
|
+
heading_2: Heading,
|
|
1130
|
+
heading_3: Heading,
|
|
1131
|
+
heading_4: Heading,
|
|
1132
|
+
bulleted_list_item: BulletedListItem,
|
|
1133
|
+
numbered_list_item: NumberedListItem,
|
|
1134
|
+
to_do: ToDo,
|
|
1135
|
+
quote: Quote,
|
|
1136
|
+
callout: Callout,
|
|
1137
|
+
toggle: Toggle,
|
|
1138
|
+
divider: Divider,
|
|
1139
|
+
equation: Equation,
|
|
1140
|
+
code: Code,
|
|
1141
|
+
table: Table,
|
|
1142
|
+
column_list: ColumnList,
|
|
1143
|
+
column: Column,
|
|
1144
|
+
synced_block: SyncedBlock,
|
|
1145
|
+
image: Image,
|
|
1146
|
+
video: Video,
|
|
1147
|
+
audio: Audio,
|
|
1148
|
+
file: File,
|
|
1149
|
+
pdf: Pdf,
|
|
1150
|
+
bookmark: Bookmark,
|
|
1151
|
+
embed: Embed,
|
|
1152
|
+
link_preview: LinkPreview,
|
|
1153
|
+
child_page: ChildPage,
|
|
1154
|
+
link_to_page: LinkToPage,
|
|
1155
|
+
table_of_contents: TableOfContents,
|
|
1156
|
+
tab: Tab,
|
|
1157
|
+
inline_equation: InlineEquation
|
|
1158
|
+
};
|
|
1159
|
+
const LIST_KINDS = {
|
|
1160
|
+
bulleted_list_item: "bulleted",
|
|
1161
|
+
numbered_list_item: "numbered",
|
|
1162
|
+
to_do: "todo"
|
|
1163
|
+
};
|
|
1164
|
+
/**
|
|
1165
|
+
* Merges consecutive list items of one kind into a group, since the API
|
|
1166
|
+
* returns each item as its own block. Other blocks stand alone.
|
|
1167
|
+
*/
|
|
1168
|
+
function groupBlocks(blocks) {
|
|
1169
|
+
const groups = [];
|
|
1170
|
+
for (const block of blocks) {
|
|
1171
|
+
const kind = LIST_KINDS[block.type];
|
|
1172
|
+
const last = groups[groups.length - 1];
|
|
1173
|
+
if (!kind) groups.push({
|
|
1174
|
+
kind: "block",
|
|
1175
|
+
block
|
|
1176
|
+
});
|
|
1177
|
+
else if (last && last.kind === kind) last.items.push(block);
|
|
1178
|
+
else groups.push({
|
|
1179
|
+
kind,
|
|
1180
|
+
items: [block]
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
return groups;
|
|
1184
|
+
}
|
|
1185
|
+
const NUMBER_FORMATS = {
|
|
1186
|
+
numbers: "list-decimal",
|
|
1187
|
+
letters: "list-[lower-alpha]",
|
|
1188
|
+
roman: "list-[lower-roman]"
|
|
1189
|
+
};
|
|
1190
|
+
const NESTED_NUMBERS = "[&_[data-list=numbers]]:list-[lower-alpha] [&_[data-list=numbers]_[data-list=numbers]]:list-[lower-roman]";
|
|
1191
|
+
const NESTED_BULLETS = "[&_[data-list=bulleted]]:list-[circle] [&_[data-list=bulleted]_[data-list=bulleted]]:list-[square]";
|
|
1192
|
+
function List({ kind, items, context }) {
|
|
1193
|
+
const rendered = items.map((item) => /* @__PURE__ */ jsx(Block, {
|
|
1194
|
+
block: item,
|
|
1195
|
+
context
|
|
1196
|
+
}, item.id));
|
|
1197
|
+
if (kind === "numbered") {
|
|
1198
|
+
const first = items[0].type === "numbered_list_item" ? items[0].numbered_list_item : void 0;
|
|
1199
|
+
const format = first?.list_format ?? "numbers";
|
|
1200
|
+
return /* @__PURE__ */ jsx("ol", {
|
|
1201
|
+
start: first?.list_start_index,
|
|
1202
|
+
"data-list": format,
|
|
1203
|
+
className: cx("flex flex-col gap-2 ps-6", NUMBER_FORMATS[format], NESTED_NUMBERS),
|
|
1204
|
+
children: rendered
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
return /* @__PURE__ */ jsx("ul", {
|
|
1208
|
+
"data-list": kind,
|
|
1209
|
+
className: cx("flex flex-col gap-2", kind === "bulleted" ? cx("list-disc ps-6 marker:text-muted-foreground", NESTED_BULLETS) : "list-none"),
|
|
1210
|
+
children: rendered
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
/** Blocks in order, with list items grouped into lists. */
|
|
1214
|
+
function BlockList({ blocks, context }) {
|
|
1215
|
+
if (!blocks?.length) return null;
|
|
1216
|
+
return groupBlocks(blocks).map((group) => group.kind === "block" ? /* @__PURE__ */ jsx(Block, {
|
|
1217
|
+
block: group.block,
|
|
1218
|
+
context
|
|
1219
|
+
}, group.block.id) : /* @__PURE__ */ jsx(List, {
|
|
1220
|
+
kind: group.kind,
|
|
1221
|
+
items: group.items,
|
|
1222
|
+
context
|
|
1223
|
+
}, group.items[0].id));
|
|
1224
|
+
}
|
|
1225
|
+
/** One block, rendered by its component, with its nested blocks as children. */
|
|
1226
|
+
function Block({ block, context }) {
|
|
1227
|
+
const Component = context.components[block.type];
|
|
1228
|
+
if (!Component) {
|
|
1229
|
+
warnOnce(`Notion block type "${block.type}" is not rendered`);
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
const children = block.children?.length ? /* @__PURE__ */ jsx(BlockList, {
|
|
1233
|
+
blocks: block.children,
|
|
1234
|
+
context
|
|
1235
|
+
}) : void 0;
|
|
1236
|
+
return /* @__PURE__ */ jsx(Component, {
|
|
1237
|
+
block,
|
|
1238
|
+
context,
|
|
1239
|
+
children
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Renders Notion blocks. Works as a Server Component; it ships no client
|
|
1244
|
+
* JavaScript of its own (demos ship theirs).
|
|
1245
|
+
*/
|
|
1246
|
+
function NotionRenderer({ blocks, components, pageHref = defaultPageHref, transformFileUrl, demos, className }) {
|
|
1247
|
+
const context = {
|
|
1248
|
+
pageHref,
|
|
1249
|
+
transformFileUrl,
|
|
1250
|
+
demos,
|
|
1251
|
+
headings: collectHeadings(blocks),
|
|
1252
|
+
components: {
|
|
1253
|
+
...defaultComponents,
|
|
1254
|
+
...components
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1258
|
+
className: cx("notionic flex flex-col gap-4 leading-[1.6] wrap-break-word text-foreground", className),
|
|
1259
|
+
children: /* @__PURE__ */ jsx(BlockList, {
|
|
1260
|
+
blocks,
|
|
1261
|
+
context
|
|
1262
|
+
})
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
//#endregion
|
|
1266
|
+
export { Block, BlockList, DemoFrame, NotionLink, NotionRenderer, RichText, blockColorClass, collectHeadings, colorClass, defaultComponents, defaultPageHref, groupBlocks, languageName, normalizePageId, pageTitle, plainText, resolveFileUrl, resolveLink, resolvePageLink };
|