camox 0.47.0 → 0.48.0
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/dist/core/createApp.d.ts +19 -6
- package/dist/core/createApp.js +7 -2
- package/dist/core/createBlock.js +54 -43
- package/dist/core/createCollection.d.ts +25 -7
- package/dist/core/createCollection.js +28 -9
- package/dist/core/createLayout.d.ts +98 -71
- package/dist/core/createLayout.js +10 -16
- package/dist/core/derivedRoutes.js +1 -1
- package/dist/core/editing/BlockEditingRuntime.d.ts +3 -2
- package/dist/core/editing/CompleteBlockEditingRuntime.js +2 -1
- package/dist/core/editing/createEditableBlock.d.ts +59 -19
- package/dist/core/editing/createEditableBlock.js +162 -151
- package/dist/core/editing/useLatestRecord.js +7 -7
- package/dist/core/layoutContext.d.ts +54 -0
- package/dist/core/layoutContext.js +66 -0
- package/dist/core/lib/contentType.d.ts +91 -4
- package/dist/core/lib/contentType.js +84 -7
- package/dist/core/lib/recordScope.js +19 -17
- package/dist/core/lib/reference.d.ts +6 -2
- package/dist/core/recordItem.js +55 -2
- package/dist/core/requestContext.d.ts +18 -0
- package/dist/core/structuredData.d.ts +126 -0
- package/dist/core/structuredData.js +40 -0
- package/dist/features/canvas/canvasCamera.js +1 -1
- package/dist/features/canvas/sidebar/CanvasSidebarFrame.js +1 -1
- package/dist/features/content/ContentCollection.js +13 -7
- package/dist/features/content/collection-form.js +6 -2
- package/dist/features/content/components/MissingFromLocale.js +1 -1
- package/dist/features/og/pageImage.js +89 -0
- package/dist/features/page/CuratedPageContent.js +45 -32
- package/dist/features/page/DerivedPageContent.js +49 -22
- package/dist/features/page/SharedLayoutContent.js +26 -22
- package/dist/features/preview/CamoxPreview.js +40 -26
- package/dist/features/preview/components/AddBlockSheet.js +1 -0
- package/dist/features/preview/components/BlockActionsPopover.js +8 -6
- package/dist/features/preview/components/PageInfo.js +244 -264
- package/dist/features/preview/components/PageTree.js +63 -53
- package/dist/features/preview/components/RecordCard.js +50 -34
- package/dist/features/preview/components/RecordCombobox.js +12 -5
- package/dist/features/preview/components/ReferenceFieldEditor.js +4 -2
- package/dist/features/preview/components/ReferenceListFieldEditor.js +24 -19
- package/dist/features/preview/components/RepeatableItemsList.js +62 -46
- package/dist/features/preview/components/SelectionEditor.js +3 -2
- package/dist/features/preview/components/useFieldWriter.js +2 -4
- package/dist/features/preview/components/useRecordView.js +2 -2
- package/dist/features/preview/entityName.js +11 -0
- package/dist/features/preview/pageRecord.js +17 -18
- package/dist/features/routes/pageRuntime.d.ts +15 -1
- package/dist/features/routes/pageRuntime.js +35 -38
- package/dist/features/runtime/RuntimeChrome.js +1 -1
- package/dist/features/runtime/breadcrumbs.js +68 -0
- package/dist/features/runtime/livePages.js +3 -2
- package/dist/features/runtime/pageGraph.js +92 -0
- package/dist/features/runtime/projectLogo.js +110 -0
- package/dist/features/runtime/runtime.d.ts +11 -6
- package/dist/features/runtime/runtime.js +278 -55
- package/dist/features/runtime/runtimeRoutes.d.ts +1 -0
- package/dist/features/runtime/runtimeRoutes.js +1 -0
- package/dist/features/runtime/structuredData.js +213 -0
- package/dist/features/studio/components/ChatSidebar.js +5 -5
- package/dist/features/studio/components/ProjectMenu.js +127 -100
- package/dist/features/studio/components/SocialProfilesDialog.js +342 -0
- package/dist/features/studio/components/ThreadMessages.js +2 -2
- package/dist/features/vite/blockBoilerplate.js +3 -2
- package/dist/features/vite/layoutGeneration.js +3 -2
- package/dist/features/vite/vite.js +1 -0
- package/dist/lib/queries.d.ts +4 -0
- package/dist/lib/queries.js +1 -0
- package/dist/lib/studio-reads.js +3 -1
- package/dist/package.js +1 -1
- package/dist/studio.css +1 -1
- package/package.json +5 -4
- package/skills/camox/references/block-definitions.md +105 -25
- package/skills/camox/references/layout-definitions.md +53 -17
- package/skills/camox/references/routed-layouts.md +17 -14
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BlockStructuredData, KnownNodesOnly, RepeatedNodes } from "../structuredData.js";
|
|
1
2
|
import { Collection } from "../createCollection.js";
|
|
2
3
|
import { IconId, IconValue } from "./iconTypes.js";
|
|
3
4
|
import { SystemOrderKey } from "@camox/api-contract";
|
|
@@ -61,7 +62,75 @@ export type ContentProxy<TShape extends Record<string, TSchema>> = { [K in keyof
|
|
|
61
62
|
*/
|
|
62
63
|
type SettingCallable<T extends TSchema> = Static<T> extends boolean ? (lines: ConditionalLines) => Conditional : Static<T> extends string ? (value: Static<T>, lines: ConditionalLines) => Conditional : never;
|
|
63
64
|
export type SettingsProxy<TShape extends Record<string, TSchema>> = { [K in keyof TShape & string]: SettingCallable<TShape[K]>; };
|
|
64
|
-
|
|
65
|
+
/** What a block's or repeatable item's templates receive: proxies of its content and settings. */
|
|
66
|
+
export interface TemplateContext<TContent extends Record<string, TSchema>, TSettings extends Record<string, TSchema> = Record<string, never>> {
|
|
67
|
+
content: ContentProxy<TContent>;
|
|
68
|
+
settings: SettingsProxy<TSettings>;
|
|
69
|
+
}
|
|
70
|
+
/** Builds a Markdown template from `{ content, settings }`. */
|
|
71
|
+
export type ToMarkdownBuilder<TContent extends Record<string, TSchema>, TSettings extends Record<string, TSchema> = Record<string, never>> = (context: TemplateContext<TContent, TSettings>) => ReadonlyArray<string | FieldToken | Conditional>;
|
|
72
|
+
/** A collection's String fields as field tokens: what its record templates can name. */
|
|
73
|
+
export type RecordTextProxy<TShape extends Record<string, TSchema>> = { [K in keyof TShape & string as TShape[K] extends {
|
|
74
|
+
fieldType: "String";
|
|
75
|
+
} ? K : never]: string & FieldToken; };
|
|
76
|
+
/** What a collection's `toTitle` and `toDescription` receive: the record's String fields. */
|
|
77
|
+
export interface RecordTemplateContext<TShape extends Record<string, TSchema>> {
|
|
78
|
+
content: RecordTextProxy<TShape>;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Builds a title template from `{ content, settings }`: one line naming a block or repeatable item
|
|
82
|
+
* until it has a Summary, such as its headline. The API evaluates it on default-locale content.
|
|
83
|
+
*/
|
|
84
|
+
export type ToTitleBuilder<TContent extends Record<string, TSchema>, TSettings extends Record<string, TSchema> = Record<string, never>> = (context: TemplateContext<TContent, TSettings>) => string | FieldToken;
|
|
85
|
+
/**
|
|
86
|
+
* An image field's token in a Structured Data template, which the API evaluates into an
|
|
87
|
+
* `ImageObject` with its absolute URL, dimensions and alt text.
|
|
88
|
+
*/
|
|
89
|
+
export type ImageToken = {
|
|
90
|
+
"@type": "ImageObject";
|
|
91
|
+
url: string;
|
|
92
|
+
contentUrl: string;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* A reference's token in a Structured Data template: a link to the record's node, which Camox
|
|
96
|
+
* adds to the page's graph, or the record's title when its collection has no `toStructuredData`.
|
|
97
|
+
*/
|
|
98
|
+
export type RecordToken = {
|
|
99
|
+
readonly "@id": string;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* What a field reads as in a Structured Data template. Text, files, links and embeds read as
|
|
103
|
+
* strings, links and files as absolute URLs. References link their records' nodes, and their
|
|
104
|
+
* fields read as the record's. Icons, artifacts and lists of files aren't Structured Data.
|
|
105
|
+
*/
|
|
106
|
+
type StructuredDataToken<T extends TSchema> = T extends ReferenceSchema<infer R> ? RecordToken & StructuredDataProxy<R> : T extends ReferenceListSchema<any> ? RecordToken[] : T extends {
|
|
107
|
+
fieldType: "Artifact";
|
|
108
|
+
} ? never : T extends TArray<TObject<any>> ? RepeatedNodes : T extends TArray<any> ? never : Static<T> extends IconValue ? never : Static<T> extends ImageValue ? ImageToken : Static<T> extends string | LinkValue | FileValue ? string & FieldToken : never;
|
|
109
|
+
/** A block's or repeatable item's fields as a Structured Data template reads them. */
|
|
110
|
+
export type StructuredDataProxy<TShape extends Record<string, TSchema>> = { [K in keyof TShape & string as [StructuredDataToken<TShape[K]>] extends [never] ? never : K]: StructuredDataToken<TShape[K]>; };
|
|
111
|
+
/**
|
|
112
|
+
* One setting in a Structured Data template: shows a part of the template only while it holds,
|
|
113
|
+
* typed as the part it shows. A Boolean takes the part; an Enum takes the option it must equal,
|
|
114
|
+
* then the part. Settings of other shapes aren't conditions.
|
|
115
|
+
*/
|
|
116
|
+
type StructuredDataSettingCallable<T extends TSchema> = Static<T> extends boolean ? <const V>(value: V) => V : Static<T> extends string ? <const V>(equals: Static<T>, value: V) => V : never;
|
|
117
|
+
/** A block's or repeatable item's settings, as its Structured Data template reads them. */
|
|
118
|
+
export type StructuredDataSettingsProxy<TShape extends Record<string, TSchema>> = { [K in keyof TShape & string]: StructuredDataSettingCallable<TShape[K]>; };
|
|
119
|
+
/** What a block's or repeatable item's `toStructuredData` receives. */
|
|
120
|
+
export interface StructuredDataContext<TContent extends Record<string, TSchema>, TSettings extends Record<string, TSchema> = Record<string, never>> {
|
|
121
|
+
content: StructuredDataProxy<TContent>;
|
|
122
|
+
settings: StructuredDataSettingsProxy<TSettings>;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Builds a block's or repeatable item's Structured Data template from `{ content, settings }`:
|
|
126
|
+
* the nodes it shows. A repeater's token places its items' nodes. The API evaluates the
|
|
127
|
+
* template on each page's content, and Camox links each node to its page.
|
|
128
|
+
*/
|
|
129
|
+
export type ToStructuredDataBuilder<TContent extends Record<string, TSchema>, TSettings extends Record<string, TSchema>, Nodes extends BlockStructuredData> = (context: StructuredDataContext<TContent, TSettings>) => Nodes & KnownNodesOnly<Nodes>;
|
|
130
|
+
/** What a collection's `toStructuredData` receives: the record's fields as tokens. */
|
|
131
|
+
export interface RecordStructuredDataContext<TShape extends Record<string, TSchema>> {
|
|
132
|
+
content: StructuredDataProxy<TShape>;
|
|
133
|
+
}
|
|
65
134
|
declare const EmbedURLBrand: unique symbol;
|
|
66
135
|
export type EmbedURL = string & {
|
|
67
136
|
readonly [EmbedURLBrand]: true;
|
|
@@ -269,7 +338,7 @@ export interface ContentFieldBuilder {
|
|
|
269
338
|
* cannot pick, remove or reorder records, and nothing is stored on the block.
|
|
270
339
|
*
|
|
271
340
|
* @example
|
|
272
|
-
* field.referenceList(customers, { maxItems: 6, toMarkdown: (
|
|
341
|
+
* field.referenceList(customers, { maxItems: 6, toMarkdown: ({ content }) => [content.name] })
|
|
273
342
|
* field.referenceList(articles, { query: { orderBy: { publishedAt: "desc" }, limit: 3 } })
|
|
274
343
|
*/
|
|
275
344
|
referenceList: <T extends Record<string, TSchema>, const L extends number = number>(collection: Collection<T>, options?: {
|
|
@@ -295,16 +364,34 @@ export interface ContentFieldBuilder {
|
|
|
295
364
|
* minItems: 1,
|
|
296
365
|
* maxItems: 10,
|
|
297
366
|
* title: "Items",
|
|
298
|
-
*
|
|
367
|
+
* toTitle: ({ content }) => content.title,
|
|
368
|
+
* toMarkdown: ({ content, settings }) => [
|
|
369
|
+
* `### ${content.title}`,
|
|
370
|
+
* settings.highlighted(content.description),
|
|
371
|
+
* ],
|
|
299
372
|
* })
|
|
300
373
|
*/
|
|
301
|
-
repeater: <T extends Record<string, TSchema>, S extends Record<string, TSchema> = Record<string, never
|
|
374
|
+
repeater: <T extends Record<string, TSchema>, S extends Record<string, TSchema> = Record<string, never>, N extends BlockStructuredData = BlockStructuredData>(options: {
|
|
302
375
|
content: (field: ContentFieldBuilder) => T & NoInfer<ArtifactDataCheck<T, S>>;
|
|
303
376
|
settings?: (setting: SettingBuilder) => S;
|
|
304
377
|
minItems: number;
|
|
305
378
|
maxItems: number;
|
|
306
379
|
title?: string;
|
|
380
|
+
/** Names each item until it has a Summary, such as its title. See `ToTitleBuilder`. */
|
|
381
|
+
toTitle: ToTitleBuilder<T, S>;
|
|
307
382
|
toMarkdown: ToMarkdownBuilder<T, S>;
|
|
383
|
+
/**
|
|
384
|
+
* The nodes each item shows, placed wherever the parent's template places the repeater's
|
|
385
|
+
* token. See `ToStructuredDataBuilder`.
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* toStructuredData: ({ content }) => ({
|
|
389
|
+
* "@type": "Question",
|
|
390
|
+
* name: content.question,
|
|
391
|
+
* acceptedAnswer: { "@type": "Answer", text: content.answer },
|
|
392
|
+
* })
|
|
393
|
+
*/
|
|
394
|
+
toStructuredData?: ToStructuredDataBuilder<T, S, N>;
|
|
308
395
|
}) => RepeaterField<T, S>;
|
|
309
396
|
}
|
|
310
397
|
/** Content fields that can't be Artifact Data: artifact sources, repeaters and references. */
|
|
@@ -48,10 +48,21 @@ function createContentProxy(shape, prefix = "") {
|
|
|
48
48
|
if (typeof prop !== "string") return void 0;
|
|
49
49
|
const path = prefix ? `${prefix}.${prop}` : prop;
|
|
50
50
|
const field = shape?.[prop];
|
|
51
|
-
if (field?.fieldType === "Reference") return
|
|
51
|
+
if (field?.fieldType === "Reference") return referenceToken(field.referenceSchema.properties, path);
|
|
52
52
|
return new FieldToken(path);
|
|
53
53
|
} });
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* A reference field's token, which names the reference itself, as Structured Data templates
|
|
57
|
+
* link records by it, and whose properties are the tokens of the record's fields.
|
|
58
|
+
*/
|
|
59
|
+
function referenceToken(shape, path) {
|
|
60
|
+
const fields = createContentProxy(shape, path);
|
|
61
|
+
return new Proxy(new FieldToken(path), { get(target, prop, receiver) {
|
|
62
|
+
if (typeof prop !== "string" || prop in target) return Reflect.get(target, prop, receiver);
|
|
63
|
+
return fields[prop];
|
|
64
|
+
} });
|
|
65
|
+
}
|
|
55
66
|
function createSettingsProxy(settingsShape, scope) {
|
|
56
67
|
return new Proxy({}, { get(_target, prop) {
|
|
57
68
|
if (typeof prop !== "string") return void 0;
|
|
@@ -69,12 +80,78 @@ function serializeConditional(cond) {
|
|
|
69
80
|
return out;
|
|
70
81
|
}
|
|
71
82
|
function resolveToMarkdown(builder, settingsShape, scope, contentShape) {
|
|
72
|
-
const entries = builder(
|
|
83
|
+
const entries = builder({
|
|
84
|
+
content: createContentProxy(contentShape),
|
|
85
|
+
settings: createSettingsProxy(settingsShape, scope)
|
|
86
|
+
});
|
|
73
87
|
const out = [];
|
|
74
88
|
for (const entry of entries) if (entry instanceof Conditional) out.push(...serializeConditional(entry));
|
|
75
89
|
else out.push(entry instanceof FieldToken ? entry.toString() : String(entry));
|
|
76
90
|
return out;
|
|
77
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Runs a collection's record template, such as `toTitle`, into the token template the API fills
|
|
94
|
+
* with each record's plain text. Null when it returns nothing.
|
|
95
|
+
*/
|
|
96
|
+
function resolveRecordTemplate(template, contentShape) {
|
|
97
|
+
const result = template({ content: createContentProxy(contentShape) });
|
|
98
|
+
return result === void 0 || result === null || String(result) === "" ? null : String(result);
|
|
99
|
+
}
|
|
100
|
+
/** The one-line title template a `toTitle` builder declares. */
|
|
101
|
+
function resolveToTitle(builder, settingsShape, scope, contentShape) {
|
|
102
|
+
const content = createContentProxy(contentShape);
|
|
103
|
+
const settings = createSettingsProxy(settingsShape, scope);
|
|
104
|
+
return String(builder({
|
|
105
|
+
content,
|
|
106
|
+
settings
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
/** A part of a Structured Data template shown only while a setting holds. */
|
|
110
|
+
var SettingCondition = class {
|
|
111
|
+
scope;
|
|
112
|
+
settingName;
|
|
113
|
+
equals;
|
|
114
|
+
show;
|
|
115
|
+
constructor(scope, settingName, equals, show) {
|
|
116
|
+
this.scope = scope;
|
|
117
|
+
this.settingName = settingName;
|
|
118
|
+
this.equals = equals;
|
|
119
|
+
this.show = show;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
function createStructuredDataSettingsProxy(settingsShape, scope) {
|
|
123
|
+
return new Proxy({}, { get(_target, prop) {
|
|
124
|
+
if (typeof prop !== "string") return void 0;
|
|
125
|
+
const fieldType = (settingsShape?.[prop])?.fieldType;
|
|
126
|
+
if (fieldType === "Boolean") return (value) => new SettingCondition(scope, prop, null, value);
|
|
127
|
+
if (fieldType === "Enum") return (equals, value) => new SettingCondition(scope, prop, equals, value);
|
|
128
|
+
throw new Error(`toStructuredData settings proxy: "${prop}" is not a Boolean or Enum setting on this ${scope}.`);
|
|
129
|
+
} });
|
|
130
|
+
}
|
|
131
|
+
/** A template's value as the Definition Set syncs it: JSON, with tokens as `{{field}}`. */
|
|
132
|
+
function serializeTemplate(value) {
|
|
133
|
+
if (value instanceof FieldToken) return value.toString();
|
|
134
|
+
if (value instanceof SettingCondition) return {
|
|
135
|
+
"@camox": "if",
|
|
136
|
+
setting: `${value.scope === "block" ? "settings" : "itemSettings"}.${value.settingName}`,
|
|
137
|
+
...value.equals !== null && { equals: value.equals },
|
|
138
|
+
show: serializeTemplate(value.show)
|
|
139
|
+
};
|
|
140
|
+
if (Array.isArray(value)) return value.map(serializeTemplate);
|
|
141
|
+
if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).flatMap(([key, entry]) => entry === void 0 ? [] : [[key, serializeTemplate(entry)]]));
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
/** The Structured Data template a `toStructuredData` builder declares. */
|
|
145
|
+
function resolveToStructuredData(builder, settingsShape, scope, contentShape) {
|
|
146
|
+
return serializeTemplate(builder({
|
|
147
|
+
content: createContentProxy(contentShape),
|
|
148
|
+
settings: createStructuredDataSettingsProxy(settingsShape, scope)
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
/** The Structured Data template of each record of a collection, as its builder declares it. */
|
|
152
|
+
function resolveRecordStructuredData(builder, contentShape) {
|
|
153
|
+
return serializeTemplate(builder({ content: createContentProxy(contentShape) }));
|
|
154
|
+
}
|
|
78
155
|
/** The longest artifact source content writes accept (256 KB). */
|
|
79
156
|
const ARTIFACT_MAX_LENGTH = 262144;
|
|
80
157
|
const IMAGE_PLACEHOLDER = Symbol("camox.imagePlaceholder");
|
|
@@ -259,8 +336,7 @@ const contentFieldBuilder = {
|
|
|
259
336
|
required: options.required ?? false,
|
|
260
337
|
title: options.title ?? collection._internal.title,
|
|
261
338
|
default: null,
|
|
262
|
-
referenceSchema: collection._internal.contentSchema
|
|
263
|
-
labelField: collection._internal.label
|
|
339
|
+
referenceSchema: collection._internal.contentSchema
|
|
264
340
|
}),
|
|
265
341
|
referenceList: (collection, options = {}) => Type.Unsafe({
|
|
266
342
|
type: "array",
|
|
@@ -275,8 +351,7 @@ const contentFieldBuilder = {
|
|
|
275
351
|
...options.maxItems === void 0 ? {} : { maxItems: options.maxItems },
|
|
276
352
|
...options.query === void 0 ? { default: [] } : { query: validateQuery(options.query, collection) },
|
|
277
353
|
...options.toMarkdown === void 0 ? {} : { toMarkdown: resolveToMarkdown(options.toMarkdown, void 0, "item", collection._internal.contentSchema.properties) },
|
|
278
|
-
referenceSchema: collection._internal.contentSchema
|
|
279
|
-
labelField: collection._internal.label
|
|
354
|
+
referenceSchema: collection._internal.contentSchema
|
|
280
355
|
}),
|
|
281
356
|
repeater: (options) => {
|
|
282
357
|
if (options.minItems < 1) throw new Error("Repeater requires minItems to be at least 1");
|
|
@@ -301,7 +376,9 @@ const contentFieldBuilder = {
|
|
|
301
376
|
default: defaultArray,
|
|
302
377
|
title: options.title,
|
|
303
378
|
fieldType: "Repeater",
|
|
379
|
+
toTitle: resolveToTitle(options.toTitle, settings, "item", content),
|
|
304
380
|
toMarkdown: resolveToMarkdown(options.toMarkdown, settings, "item", content),
|
|
381
|
+
...options.toStructuredData && { toStructuredData: resolveToStructuredData(options.toStructuredData, settings, "item", content) },
|
|
305
382
|
itemSettingsSchema,
|
|
306
383
|
defaultItemSettings: settingsObjectSchema ? collectSchemaDefaults(settingsObjectSchema.properties) : void 0
|
|
307
384
|
});
|
|
@@ -344,4 +421,4 @@ function resolveBlockFields(options) {
|
|
|
344
421
|
}
|
|
345
422
|
|
|
346
423
|
//#endregion
|
|
347
|
-
export { ARTIFACT_MAX_LENGTH, Conditional, FieldToken, assertFieldKinds, collectionField, collectionFieldBuilder, contentFieldBuilder, getImagePlaceholderTitle, isImagePlaceholder, resolveBlockFields, resolveToMarkdown, settingBuilder };
|
|
424
|
+
export { ARTIFACT_MAX_LENGTH, Conditional, FieldToken, assertFieldKinds, collectionField, collectionFieldBuilder, contentFieldBuilder, getImagePlaceholderTitle, isImagePlaceholder, resolveBlockFields, resolveRecordStructuredData, resolveRecordTemplate, resolveToMarkdown, resolveToStructuredData, resolveToTitle, settingBuilder };
|
|
@@ -250,7 +250,7 @@ const ReferenceList = (t0) => {
|
|
|
250
250
|
* (the second hop), which link nothing further.
|
|
251
251
|
*/
|
|
252
252
|
function RecordScope(t0) {
|
|
253
|
-
const $ = c(
|
|
253
|
+
const $ = c(15);
|
|
254
254
|
const { record, properties, hop, children } = t0;
|
|
255
255
|
let t1;
|
|
256
256
|
if ($[0] !== properties || $[1] !== record.content) {
|
|
@@ -265,10 +265,11 @@ function RecordScope(t0) {
|
|
|
265
265
|
const value = t1;
|
|
266
266
|
const t2 = hop === 1 ? record.references : void 0;
|
|
267
267
|
let t3;
|
|
268
|
-
if ($[3] !== children || $[4] !== hop || $[5] !== record.
|
|
268
|
+
if ($[3] !== children || $[4] !== hop || $[5] !== record.description || $[6] !== record.id || $[7] !== record.title) {
|
|
269
269
|
t3 = children({
|
|
270
270
|
id: record.id,
|
|
271
|
-
|
|
271
|
+
title: record.title,
|
|
272
|
+
description: record.description,
|
|
272
273
|
Field,
|
|
273
274
|
Image,
|
|
274
275
|
File,
|
|
@@ -282,30 +283,31 @@ function RecordScope(t0) {
|
|
|
282
283
|
});
|
|
283
284
|
$[3] = children;
|
|
284
285
|
$[4] = hop;
|
|
285
|
-
$[5] = record.
|
|
286
|
-
$[6] = record.
|
|
287
|
-
$[7] =
|
|
288
|
-
|
|
286
|
+
$[5] = record.description;
|
|
287
|
+
$[6] = record.id;
|
|
288
|
+
$[7] = record.title;
|
|
289
|
+
$[8] = t3;
|
|
290
|
+
} else t3 = $[8];
|
|
289
291
|
let t4;
|
|
290
|
-
if ($[
|
|
292
|
+
if ($[9] !== t2 || $[10] !== t3) {
|
|
291
293
|
t4 = /* @__PURE__ */ jsx(LinksContext.Provider, {
|
|
292
294
|
value: t2,
|
|
293
295
|
children: t3
|
|
294
296
|
});
|
|
295
|
-
$[
|
|
296
|
-
$[
|
|
297
|
-
$[
|
|
298
|
-
} else t4 = $[
|
|
297
|
+
$[9] = t2;
|
|
298
|
+
$[10] = t3;
|
|
299
|
+
$[11] = t4;
|
|
300
|
+
} else t4 = $[11];
|
|
299
301
|
let t5;
|
|
300
|
-
if ($[
|
|
302
|
+
if ($[12] !== t4 || $[13] !== value) {
|
|
301
303
|
t5 = /* @__PURE__ */ jsx(RecordContext.Provider, {
|
|
302
304
|
value,
|
|
303
305
|
children: t4
|
|
304
306
|
});
|
|
305
|
-
$[
|
|
306
|
-
$[
|
|
307
|
-
$[
|
|
308
|
-
} else t5 = $[
|
|
307
|
+
$[12] = t4;
|
|
308
|
+
$[13] = value;
|
|
309
|
+
$[14] = t5;
|
|
310
|
+
} else t5 = $[14];
|
|
309
311
|
return t5;
|
|
310
312
|
}
|
|
311
313
|
function _temp(location) {
|
|
@@ -29,7 +29,10 @@ type LinkedRecords<T extends Record<string, TSchema>> = {
|
|
|
29
29
|
};
|
|
30
30
|
type RecordFields<T extends Record<string, TSchema>> = {
|
|
31
31
|
id: string;
|
|
32
|
-
|
|
32
|
+
/** The record's title, from its collection's `toTitle`. */
|
|
33
|
+
title: string;
|
|
34
|
+
/** The record's description, from its collection's `toDescription`, or null for none. */
|
|
35
|
+
description: string | null;
|
|
33
36
|
Field: (props: InlineTextStyles & {
|
|
34
37
|
name: TextKeys<T>;
|
|
35
38
|
children: (props: React.HTMLAttributes<HTMLElement> & {
|
|
@@ -70,7 +73,8 @@ export type ReferenceContent<T extends TSchema> = T extends ReferenceSchema<infe
|
|
|
70
73
|
export type ReferenceRecord = {
|
|
71
74
|
id: string;
|
|
72
75
|
collectionId: string;
|
|
73
|
-
|
|
76
|
+
title: string;
|
|
77
|
+
description: string | null;
|
|
74
78
|
content: Record<string, unknown>;
|
|
75
79
|
/** When its content last changed, for a record page's record. */
|
|
76
80
|
updatedAt?: number;
|
package/dist/core/recordItem.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveToMarkdown } from "./lib/contentType.js";
|
|
2
2
|
import { isLexicalState, lexicalStateToPlainText } from "./lib/lexicalState.js";
|
|
3
|
+
import { recordLink, recordText, templateFields } from "@camox/api-contract";
|
|
3
4
|
|
|
4
5
|
//#region src/core/recordItem.ts
|
|
5
6
|
/**
|
|
@@ -17,6 +18,58 @@ function recordPlainText(collection, content) {
|
|
|
17
18
|
for (const [name, field] of Object.entries(properties)) if (field.fieldType === "String") text[name] = fieldPlainText(content[name]);
|
|
18
19
|
return text;
|
|
19
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* A record's references as links to their records' nodes: one for a reference, which is absent
|
|
23
|
+
* when it resolves to nothing, and a list for a reference list.
|
|
24
|
+
*/
|
|
25
|
+
function recordLinks(collection, references) {
|
|
26
|
+
const properties = collection._internal.contentSchema.properties;
|
|
27
|
+
const links = {};
|
|
28
|
+
for (const [name, field] of Object.entries(properties)) {
|
|
29
|
+
const resolved = references?.[name];
|
|
30
|
+
if (field.fieldType === "ReferenceList") links[name] = (Array.isArray(resolved) ? resolved : []).map(({ id }) => recordLink(id));
|
|
31
|
+
if (field.fieldType === "Reference" && resolved && !Array.isArray(resolved)) links[name] = recordLink(resolved.id);
|
|
32
|
+
}
|
|
33
|
+
return links;
|
|
34
|
+
}
|
|
35
|
+
/** The titles of the records a record references, by record id. */
|
|
36
|
+
function referencedTitles(references) {
|
|
37
|
+
return Object.fromEntries(Object.values(references ?? {}).flatMap((value) => [value ?? []].flat()).map((record) => [record.id, record.title]));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The record a record page renders, as its layout's context names it, from the record a loader
|
|
41
|
+
* found and when it was published to Live.
|
|
42
|
+
*/
|
|
43
|
+
function recordPageValues(collection, { data: record, published }) {
|
|
44
|
+
return {
|
|
45
|
+
title: record.title ?? "",
|
|
46
|
+
description: record.description ?? void 0,
|
|
47
|
+
content: {
|
|
48
|
+
...recordLinks(collection, record.references),
|
|
49
|
+
...recordPlainText(collection, record.content)
|
|
50
|
+
},
|
|
51
|
+
titles: referencedTitles(record.references),
|
|
52
|
+
...published && { published }
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A record's title and description: its collection's templates filled with its fields' plain
|
|
57
|
+
* text, as the API fills them. For records edited here, before the API names them again.
|
|
58
|
+
*/
|
|
59
|
+
function recordText$1(collection, content) {
|
|
60
|
+
if (!collection) return {
|
|
61
|
+
title: "",
|
|
62
|
+
description: null
|
|
63
|
+
};
|
|
64
|
+
return recordText(collection._internal, (name) => fieldPlainText(content[name]));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The first field a collection's title template names, such as `firstName` in
|
|
68
|
+
* `{{firstName}} {{lastName}}`: where a new record's name goes.
|
|
69
|
+
*/
|
|
70
|
+
function titleField(toTitle) {
|
|
71
|
+
return templateFields(toTitle)[0];
|
|
72
|
+
}
|
|
20
73
|
/** A String field's text, whether it is stored as plain text or rich-text state. */
|
|
21
74
|
function fieldPlainText(value) {
|
|
22
75
|
if (typeof value !== "string" && !isLexicalState(value)) return "";
|
|
@@ -27,8 +80,8 @@ function fieldPlainText(value) {
|
|
|
27
80
|
* field tokens are left for the API to fill with the live record.
|
|
28
81
|
*/
|
|
29
82
|
function recordMarkdownTemplate(collection, toMarkdown) {
|
|
30
|
-
return resolveToMarkdown((
|
|
83
|
+
return resolveToMarkdown(({ content }) => toMarkdown(content), void 0, "item", collection._internal.contentSchema.properties);
|
|
31
84
|
}
|
|
32
85
|
|
|
33
86
|
//#endregion
|
|
34
|
-
export { asCollectionItem, fieldPlainText, recordMarkdownTemplate, recordPlainText };
|
|
87
|
+
export { asCollectionItem, fieldPlainText, recordMarkdownTemplate, recordPageValues, recordPlainText, recordText$1 as recordText, titleField };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ReferenceRecord } from "./lib/reference.js";
|
|
2
|
+
import { EvaluatedNode, RecordStructuredData } from "@camox/api-contract";
|
|
2
3
|
//#region src/core/requestContext.d.ts
|
|
3
4
|
declare const requestContextBrand: unique symbol;
|
|
4
5
|
/**
|
|
@@ -16,11 +17,28 @@ export interface CollectionRecordId {
|
|
|
16
17
|
/** A collection record as a record page loads it. */
|
|
17
18
|
export interface LoadedRecord<Content = Record<string, unknown>> {
|
|
18
19
|
id: string;
|
|
20
|
+
/** Its title, as the API evaluates its collection's `toTitle`. */
|
|
21
|
+
title?: string;
|
|
22
|
+
/** Its description, as the API evaluates its collection's `toDescription`: null for none. */
|
|
23
|
+
description?: string | null;
|
|
19
24
|
content: Content;
|
|
20
25
|
/** The records it links, resolved by Camox with the record as the first hop. */
|
|
21
26
|
references?: ReferenceRecord["references"];
|
|
22
27
|
/** When its content last changed. */
|
|
23
28
|
updatedAt?: number;
|
|
29
|
+
/** When it was first and last published to Live. Absent from older APIs. */
|
|
30
|
+
firstPublishedAt?: number | null;
|
|
31
|
+
publishedAt?: number | null;
|
|
32
|
+
/** Its Structured Data, as the API evaluates it. Absent from older APIs. */
|
|
33
|
+
structuredData?: RecordPageStructuredData;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A record page's record as Structured Data: its node, when its collection has a template, and
|
|
37
|
+
* the nodes of the records it references and of those they link.
|
|
38
|
+
*/
|
|
39
|
+
export interface RecordPageStructuredData {
|
|
40
|
+
node?: EvaluatedNode;
|
|
41
|
+
records: RecordStructuredData;
|
|
24
42
|
}
|
|
25
43
|
//#endregion
|
|
26
44
|
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { ARTICLE_TYPES } from "@camox/api-contract";
|
|
2
|
+
import { AggregateRatingSimple, ArticleSimple, BookSimple, CourseSimple, EventSimple, HowToSimple, ImageSimple, JobPostingSimple, OfferSimple, OrganizationSimple, PersonSimple, PlaceSimple, ProductSimple, QuestionSimple, RecipeSimple, ReviewSimple, ServiceSimple, SoftwareAppSimple, VideoSimple, WebPageSimple } from "@unhead/schema-org";
|
|
3
|
+
//#region src/core/structuredData.d.ts
|
|
4
|
+
export type ArticleType = (typeof ARTICLE_TYPES)[number];
|
|
5
|
+
/** The kinds of web page a layout can say its pages are. */
|
|
6
|
+
export declare const WEB_PAGE_TYPES: readonly ["AboutPage", "CheckoutPage", "CollectionPage", "ContactPage", "FAQPage", "ItemPage", "MedicalWebPage", "ProfilePage", "QAPage", "RealEstateListing", "SearchResultsPage"];
|
|
7
|
+
export type WebPageType = (typeof WEB_PAGE_TYPES)[number];
|
|
8
|
+
/** A type's own properties, without the index signature that would let any key through. */
|
|
9
|
+
type Known<T> = T extends unknown ? { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K]; } : never;
|
|
10
|
+
/** The given properties of a type, required. */
|
|
11
|
+
type Need<T, K extends keyof Known<T>> = { [P in K]-?: Exclude<Known<T>[P], undefined>; };
|
|
12
|
+
/** Each curated `@type`, with the node whose properties it takes. */
|
|
13
|
+
type NodeProperties = { [T in ArticleType]: ArticleSimple; } & {
|
|
14
|
+
Product: ProductSimple;
|
|
15
|
+
Offer: OfferSimple;
|
|
16
|
+
Event: EventSimple;
|
|
17
|
+
Person: PersonSimple;
|
|
18
|
+
Place: PlaceSimple;
|
|
19
|
+
Organization: OrganizationSimple;
|
|
20
|
+
Review: ReviewSimple;
|
|
21
|
+
AggregateRating: AggregateRatingSimple;
|
|
22
|
+
Question: QuestionSimple;
|
|
23
|
+
HowTo: HowToSimple;
|
|
24
|
+
Recipe: RecipeSimple;
|
|
25
|
+
Course: CourseSimple;
|
|
26
|
+
JobPosting: JobPostingSimple;
|
|
27
|
+
SoftwareApplication: SoftwareAppSimple;
|
|
28
|
+
VideoObject: VideoSimple;
|
|
29
|
+
ImageObject: ImageSimple;
|
|
30
|
+
Book: BookSimple;
|
|
31
|
+
Service: ServiceSimple;
|
|
32
|
+
};
|
|
33
|
+
/** Every `@type` a definition's nodes can have. */
|
|
34
|
+
export type StructuredDataType = keyof NodeProperties;
|
|
35
|
+
/** What search engines require of each type, including what Camox may fill in. */
|
|
36
|
+
type NodeRequirements = { [T in ArticleType]: Need<ArticleSimple, "headline">; } & {
|
|
37
|
+
Product: Need<ProductSimple, "name"> & (Need<ProductSimple, "offers"> | Need<ProductSimple, "review"> | Need<ProductSimple, "aggregateRating">);
|
|
38
|
+
Offer: {
|
|
39
|
+
price: number | string;
|
|
40
|
+
priceCurrency: string;
|
|
41
|
+
};
|
|
42
|
+
Event: Need<EventSimple, "name" | "startDate" | "location">;
|
|
43
|
+
Person: Need<PersonSimple, "name">;
|
|
44
|
+
Place: Need<PlaceSimple, "name">;
|
|
45
|
+
Organization: Need<OrganizationSimple, "name">;
|
|
46
|
+
Review: Need<ReviewSimple, "author">;
|
|
47
|
+
AggregateRating: Need<AggregateRatingSimple, "ratingValue"> & (Need<AggregateRatingSimple, "ratingCount"> | Need<AggregateRatingSimple, "reviewCount">);
|
|
48
|
+
Question: Need<QuestionSimple, "name" | "acceptedAnswer">;
|
|
49
|
+
HowTo: Need<HowToSimple, "name" | "step">;
|
|
50
|
+
Recipe: Need<RecipeSimple, "name" | "image">;
|
|
51
|
+
Course: Need<CourseSimple, "name" | "description">;
|
|
52
|
+
JobPosting: Need<JobPostingSimple, "title" | "description" | "datePosted" | "hiringOrganization">;
|
|
53
|
+
SoftwareApplication: Need<SoftwareAppSimple, "name" | "offers"> & (Need<SoftwareAppSimple, "aggregateRating"> | Need<SoftwareAppSimple, "review">);
|
|
54
|
+
VideoObject: Need<VideoSimple, "name" | "thumbnailUrl" | "uploadDate">;
|
|
55
|
+
ImageObject: Need<ImageSimple, "contentUrl">;
|
|
56
|
+
Book: Need<BookSimple, "name" | "author">;
|
|
57
|
+
Service: Need<ServiceSimple, "name">;
|
|
58
|
+
};
|
|
59
|
+
/** Properties Camox sets on every node: its identity, and its link to its page. */
|
|
60
|
+
type CamoxOwned = "@type" | "@id" | "mainEntityOfPage";
|
|
61
|
+
/** A requirement, without the properties Camox fills. */
|
|
62
|
+
type Unfilled<Requirement, Filled extends PropertyKey> = Requirement extends unknown ? Omit<Requirement, Filled> : never;
|
|
63
|
+
type NodeOf<T extends StructuredDataType, Filled extends PropertyKey> = T extends unknown ? {
|
|
64
|
+
"@type": T;
|
|
65
|
+
} & Partial<Omit<Known<NodeProperties[T]>, CamoxOwned>> & Unfilled<NodeRequirements[T], Filled> : never;
|
|
66
|
+
/**
|
|
67
|
+
* A node of Camox's vocabulary. `Excluded` lists the types a definition can't emit, and `Filled`
|
|
68
|
+
* the properties Camox fills when the definition leaves them out.
|
|
69
|
+
*/
|
|
70
|
+
export type StructuredDataNode<Excluded extends StructuredDataType = never, Filled extends PropertyKey = never> = NodeOf<Exclude<StructuredDataType, Excluded>, Filled>;
|
|
71
|
+
/** The properties Camox fills on a layout's main entity, from its page. */
|
|
72
|
+
export type MainEntityFilled = "name" | "headline" | "description" | "url" | "image" | "datePublished" | "dateModified" | "publisher" | "inLanguage";
|
|
73
|
+
/** What a page is about, beside its own node. Only collections' records can be organizations. */
|
|
74
|
+
export type MainEntity = StructuredDataNode<"Organization", MainEntityFilled>;
|
|
75
|
+
/** The properties Camox fills on a record's node, from the record's title and description. */
|
|
76
|
+
export type RecordNodeFilled = "name" | "headline" | "description";
|
|
77
|
+
/**
|
|
78
|
+
* What a collection's `toStructuredData` describes each record as. Records can be organizations,
|
|
79
|
+
* such as customers or partners, with their own `@id`s.
|
|
80
|
+
*/
|
|
81
|
+
export type RecordNode = StructuredDataNode<never, RecordNodeFilled>;
|
|
82
|
+
/** What blocks and repeatable items describe: anything but an organization, as Camox emits the site's. */
|
|
83
|
+
export type BlockNode = StructuredDataNode<"Organization">;
|
|
84
|
+
declare const RepeatedNodesBrand: unique symbol;
|
|
85
|
+
/**
|
|
86
|
+
* A repeater's token in a Structured Data template: its items' nodes, from their own template,
|
|
87
|
+
* where the token is placed.
|
|
88
|
+
*/
|
|
89
|
+
export type RepeatedNodes = never[] & {
|
|
90
|
+
readonly [RepeatedNodesBrand]: true;
|
|
91
|
+
};
|
|
92
|
+
/** What a block's or repeatable item's `toStructuredData` returns: one or more nodes. */
|
|
93
|
+
export type BlockStructuredData = BlockNode | RepeatedNodes | ReadonlyArray<BlockNode | RepeatedNodes>;
|
|
94
|
+
/** Requires nothing of a list of nodes, or a node, that only take their types' properties. */
|
|
95
|
+
export type KnownNodesOnly<Nodes> = [Nodes] extends [readonly (infer Node)[]] ? KnownPropertiesOnly<Node, BlockNode> : KnownPropertiesOnly<Nodes, BlockNode>;
|
|
96
|
+
/** The page node's properties that Camox alone sets, from the page and the layout's outputs. */
|
|
97
|
+
type PageOwned = CamoxOwned | "url" | "name" | "description" | "isPartOf" | "inLanguage" | "datePublished" | "dateModified" | "primaryImageOfPage" | "breadcrumb" | "mainEntity" | "potentialAction";
|
|
98
|
+
/** A kind of web page, merged into the page's node. */
|
|
99
|
+
export type WebPageKind = {
|
|
100
|
+
"@type": WebPageType;
|
|
101
|
+
} & Partial<Omit<Known<WebPageSimple>, PageOwned>>;
|
|
102
|
+
/** What a layout's `toStructuredData` says its page is: a kind of web page, or a main entity. */
|
|
103
|
+
export type LayoutStructuredData = WebPageKind | MainEntity;
|
|
104
|
+
/** The members of a vocabulary a node of type `T` can be. */
|
|
105
|
+
type MembersOf<Vocabulary, T> = Vocabulary extends {
|
|
106
|
+
"@type": infer U;
|
|
107
|
+
} ? T extends U ? Vocabulary : never : never;
|
|
108
|
+
/**
|
|
109
|
+
* A node's keys that hold a value. TypeScript gives each object of a list the others' keys as
|
|
110
|
+
* `?: undefined`, which say nothing.
|
|
111
|
+
*/
|
|
112
|
+
type SetKeys<Node> = { [K in keyof Node]-?: [Exclude<Node[K], undefined>] extends [never] ? never : K; }[keyof Node];
|
|
113
|
+
/**
|
|
114
|
+
* The properties of a node that its type doesn't take. Object literals returned by a function
|
|
115
|
+
* escape TypeScript's excess property checks, so definitions infer their nodes and check them
|
|
116
|
+
* with `KnownPropertiesOnly`.
|
|
117
|
+
*/
|
|
118
|
+
export type UnknownProperties<Node, Vocabulary> = Node extends {
|
|
119
|
+
"@type": infer T;
|
|
120
|
+
} ? Exclude<SetKeys<Node>, keyof MembersOf<Vocabulary, T>> : never;
|
|
121
|
+
/** Requires nothing of a definition whose nodes only take their types' properties. */
|
|
122
|
+
export type KnownPropertiesOnly<Node, Vocabulary> = [UnknownProperties<Node, Vocabulary>] extends [never] ? unknown : {
|
|
123
|
+
"❌ Camox: these structured data properties aren't in Camox's vocabulary": UnknownProperties<Node, Vocabulary>;
|
|
124
|
+
};
|
|
125
|
+
//#endregion
|
|
126
|
+
export { ARTICLE_TYPES };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ARTICLE_TYPES } from "@camox/api-contract";
|
|
2
|
+
|
|
3
|
+
//#region src/core/structuredData.ts
|
|
4
|
+
/** The kinds of web page a layout can say its pages are. */
|
|
5
|
+
const WEB_PAGE_TYPES = [
|
|
6
|
+
"AboutPage",
|
|
7
|
+
"CheckoutPage",
|
|
8
|
+
"CollectionPage",
|
|
9
|
+
"ContactPage",
|
|
10
|
+
"FAQPage",
|
|
11
|
+
"ItemPage",
|
|
12
|
+
"MedicalWebPage",
|
|
13
|
+
"ProfilePage",
|
|
14
|
+
"QAPage",
|
|
15
|
+
"RealEstateListing",
|
|
16
|
+
"SearchResultsPage"
|
|
17
|
+
];
|
|
18
|
+
/** The curated types that are creative works, which pages contain rather than talk about. */
|
|
19
|
+
const CREATIVE_WORK_TYPES = [
|
|
20
|
+
...ARTICLE_TYPES,
|
|
21
|
+
"Book",
|
|
22
|
+
"Course",
|
|
23
|
+
"HowTo",
|
|
24
|
+
"ImageObject",
|
|
25
|
+
"Question",
|
|
26
|
+
"Recipe",
|
|
27
|
+
"Review",
|
|
28
|
+
"SoftwareApplication",
|
|
29
|
+
"VideoObject"
|
|
30
|
+
];
|
|
31
|
+
const includes = (types, type) => types.includes(type);
|
|
32
|
+
/** Whether a node is a kind of web page. */
|
|
33
|
+
const isWebPageKind = (node) => includes(WEB_PAGE_TYPES, node["@type"]);
|
|
34
|
+
/** Whether a node is an Article. */
|
|
35
|
+
const isArticle = (node) => includes(ARTICLE_TYPES, node["@type"]);
|
|
36
|
+
/** Whether a node is a creative work, rather than a thing a page talks about. */
|
|
37
|
+
const isCreativeWork = (node) => includes(CREATIVE_WORK_TYPES, node["@type"]);
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
export { ARTICLE_TYPES, CREATIVE_WORK_TYPES, WEB_PAGE_TYPES, isArticle, isCreativeWork, isWebPageKind };
|
|
@@ -24,7 +24,7 @@ const MAX_CANVAS_ZOOM = 2;
|
|
|
24
24
|
*/
|
|
25
25
|
const MIN_CANVAS_ZOOM = .1;
|
|
26
26
|
/** Log-scale zoom per pixel of Ctrl/⌘ + wheel, including trackpad pinches. */
|
|
27
|
-
const CANVAS_WHEEL_ZOOM_RATE = .
|
|
27
|
+
const CANVAS_WHEEL_ZOOM_RATE = .0139392;
|
|
28
28
|
/** Relative (log-scale) bands make the detent feel the same at every page width. */
|
|
29
29
|
const ZOOM_SNAP_CAPTURE = .168;
|
|
30
30
|
const ZOOM_SNAP_RELEASE = .216;
|