@coywolf/emdash 0.0.0-stage → 0.35.14
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/LICENSE +21 -0
- package/README.md +203 -2
- package/docs/README.md +45 -0
- package/docs/ai.md +39 -0
- package/docs/backups.md +88 -0
- package/docs/breadcrumbs.md +59 -0
- package/docs/code-blocks.md +21 -0
- package/docs/content-urls.md +41 -0
- package/docs/custom-blocks.md +27 -0
- package/docs/discovery.md +32 -0
- package/docs/files.md +44 -0
- package/docs/form-uploads.md +59 -0
- package/docs/headings.md +37 -0
- package/docs/images.md +141 -0
- package/docs/internals.md +13 -0
- package/docs/links.md +34 -0
- package/docs/performance.md +72 -0
- package/docs/redirects.md +84 -0
- package/docs/releasing.md +21 -0
- package/docs/reviews.md +89 -0
- package/docs/robots.md +81 -0
- package/docs/schema.md +79 -0
- package/docs/search.md +114 -0
- package/docs/videos.md +48 -0
- package/docs/wordpress-import.md +198 -0
- package/package.json +79 -5
- package/src/admin/admin-css.ts +57 -0
- package/src/admin/ai.tsx +915 -0
- package/src/admin/backups.tsx +613 -0
- package/src/admin/breadcrumbs.tsx +542 -0
- package/src/admin/codeBlocks.tsx +204 -0
- package/src/admin/customBlocks.tsx +321 -0
- package/src/admin/dirty.ts +20 -0
- package/src/admin/discovery.tsx +463 -0
- package/src/admin/download.ts +28 -0
- package/src/admin/feature-switches.tsx +124 -0
- package/src/admin/features.tsx +142 -0
- package/src/admin/files.tsx +791 -0
- package/src/admin/formUploads.tsx +445 -0
- package/src/admin/guides.tsx +18 -0
- package/src/admin/headings.tsx +262 -0
- package/src/admin/images.tsx +825 -0
- package/src/admin/index.tsx +62 -0
- package/src/admin/links.tsx +980 -0
- package/src/admin/performance.tsx +330 -0
- package/src/admin/preview.tsx +63 -0
- package/src/admin/redirects-removed.tsx +266 -0
- package/src/admin/redirects.tsx +470 -0
- package/src/admin/reviews.tsx +232 -0
- package/src/admin/robots-bots.tsx +647 -0
- package/src/admin/robots-shared.tsx +109 -0
- package/src/admin/robots-wizard.tsx +748 -0
- package/src/admin/robots.tsx +911 -0
- package/src/admin/save-bar.tsx +134 -0
- package/src/admin/schema.tsx +1187 -0
- package/src/admin/search.tsx +355 -0
- package/src/admin/settings-ui.tsx +87 -0
- package/src/admin/trash-prompt-core.ts +135 -0
- package/src/admin/trash-prompt.tsx +156 -0
- package/src/admin/usage.tsx +301 -0
- package/src/admin/videos-display.tsx +248 -0
- package/src/admin/videos.tsx +1202 -0
- package/src/admin/wpImport.tsx +1516 -0
- package/src/ai/entities.ts +59 -0
- package/src/ai/export.ts +61 -0
- package/src/ai/logic.ts +606 -0
- package/src/ai/pack.ts +121 -0
- package/src/ai/prompts.ts +87 -0
- package/src/ai/providers.ts +276 -0
- package/src/ai/routes.ts +359 -0
- package/src/ai/store.ts +301 -0
- package/src/ai/wikidata.ts +87 -0
- package/src/ai/worker.ts +530 -0
- package/src/astro/astro-modules.d.ts +6 -0
- package/src/astro/breadcrumbs/Breadcrumbs.astro +129 -0
- package/src/astro/breadcrumbs/BreadcrumbsBlock.astro +12 -0
- package/src/astro/codeBlocks/CodeBlock.astro +62 -0
- package/src/astro/codeBlocks/EmDashCode.astro +61 -0
- package/src/astro/customBlocks/CustomBlock.astro +64 -0
- package/src/astro/customBlocks/Details.astro +8 -0
- package/src/astro/customBlocks/Disclosure.astro +8 -0
- package/src/astro/customBlocks/Note.astro +8 -0
- package/src/astro/customBlocks/Podcast.astro +8 -0
- package/src/astro/customBlocks/Quote.astro +8 -0
- package/src/astro/customBlocks/Testimonial.astro +8 -0
- package/src/astro/files/FileCard.astro +103 -0
- package/src/astro/headings/HeadingBlock.astro +129 -0
- package/src/astro/headings/TableOfContents.astro +45 -0
- package/src/astro/headings/request.ts +19 -0
- package/src/astro/images/ResponsiveImage.astro +60 -0
- package/src/astro/index.ts +84 -0
- package/src/astro/reviews/CoywolfReview.astro +27 -0
- package/src/astro/reviews/Review.astro +49 -0
- package/src/astro/search/SearchBox.astro +61 -0
- package/src/astro/search/SearchBoxForm.astro +509 -0
- package/src/astro/tables/CaptionedTable.astro +72 -0
- package/src/astro/videos/CoywolfVideo.astro +448 -0
- package/src/backups/download.ts +112 -0
- package/src/backups/dump.ts +288 -0
- package/src/backups/module.ts +276 -0
- package/src/backups/pack.ts +33 -0
- package/src/backups/restore.ts +209 -0
- package/src/backups/store.ts +409 -0
- package/src/breadcrumbs/pack.ts +113 -0
- package/src/breadcrumbs/settings.ts +102 -0
- package/src/breadcrumbs/stamp.ts +40 -0
- package/src/breadcrumbs/trail.ts +123 -0
- package/src/codeBlocks/contrast.ts +179 -0
- package/src/codeBlocks/hljs.d.ts +5 -0
- package/src/codeBlocks/pack.ts +110 -0
- package/src/codeBlocks/render.ts +304 -0
- package/src/codeBlocks/settings.ts +18 -0
- package/src/codeBlocks/themes.generated.ts +304 -0
- package/src/codeBlocks/themes.ts +114 -0
- package/src/core/compose.ts +244 -0
- package/src/core/content-url.ts +888 -0
- package/src/core/csv.ts +119 -0
- package/src/core/d1-batch.ts +65 -0
- package/src/core/entry-docs.ts +175 -0
- package/src/core/features-module.ts +48 -0
- package/src/core/features.ts +392 -0
- package/src/core/generation.ts +88 -0
- package/src/core/guide-content.ts +166 -0
- package/src/core/module.ts +104 -0
- package/src/core/read-failures.ts +57 -0
- package/src/core/request-memo.ts +71 -0
- package/src/core/secrets.ts +83 -0
- package/src/core/settings-enhance.ts +74 -0
- package/src/core/site.ts +36 -0
- package/src/core/storage.ts +27 -0
- package/src/customBlocks/pack.ts +260 -0
- package/src/customBlocks/render.ts +424 -0
- package/src/customBlocks/rich.ts +313 -0
- package/src/customBlocks/settings.ts +46 -0
- package/src/discovery/build.ts +252 -0
- package/src/discovery/feed-build.ts +217 -0
- package/src/discovery/feed.ts +172 -0
- package/src/discovery/indexnow.ts +114 -0
- package/src/discovery/llms.ts +79 -0
- package/src/discovery/markdown.ts +294 -0
- package/src/discovery/memory-cache.ts +97 -0
- package/src/discovery/middleware.ts +186 -0
- package/src/discovery/module.ts +353 -0
- package/src/discovery/news.ts +99 -0
- package/src/discovery/pack.ts +58 -0
- package/src/discovery/settings.ts +91 -0
- package/src/discovery/urls.ts +35 -0
- package/src/env.d.ts +10 -0
- package/src/files/card.ts +231 -0
- package/src/files/counts.ts +60 -0
- package/src/files/export.ts +44 -0
- package/src/files/format.ts +163 -0
- package/src/files/module.ts +596 -0
- package/src/files/pack.ts +92 -0
- package/src/files/r2-rules.ts +41 -0
- package/src/files/range.ts +50 -0
- package/src/files/s3.ts +114 -0
- package/src/files/serve.ts +141 -0
- package/src/files/sigv4.ts +160 -0
- package/src/files/site.ts +137 -0
- package/src/files/walker.ts +58 -0
- package/src/formUploads/lib.ts +266 -0
- package/src/formUploads/pack.ts +55 -0
- package/src/formUploads/wrap.ts +592 -0
- package/src/forms.ts +16 -0
- package/src/headings/pack.ts +154 -0
- package/src/headings/render.ts +139 -0
- package/src/headings/settings.ts +117 -0
- package/src/headings/slug.ts +68 -0
- package/src/headings/stamp.ts +142 -0
- package/src/headings/test-hooks.mjs +14 -0
- package/src/headings/toc.ts +48 -0
- package/src/images/cloudflare.ts +329 -0
- package/src/images/lib.ts +221 -0
- package/src/images/module.ts +372 -0
- package/src/images/pack.ts +804 -0
- package/src/images/pending.ts +37 -0
- package/src/images/settings.ts +48 -0
- package/src/images/setup.ts +192 -0
- package/src/images/variants-estimate.ts +162 -0
- package/src/images/variants-store.ts +475 -0
- package/src/images/variants.ts +311 -0
- package/src/index.ts +143 -0
- package/src/links/check.ts +101 -0
- package/src/links/classify.ts +270 -0
- package/src/links/export.ts +59 -0
- package/src/links/module.ts +652 -0
- package/src/links/pack.ts +43 -0
- package/src/links/pt.ts +325 -0
- package/src/links/store.ts +704 -0
- package/src/middleware.ts +287 -0
- package/src/modules.ts +53 -0
- package/src/options.ts +66 -0
- package/src/pageCache/lib.ts +274 -0
- package/src/pageCache/pack.ts +240 -0
- package/src/pageCache/usage-core.ts +152 -0
- package/src/pageCache/usage.ts +262 -0
- package/src/pageCache/warm.ts +722 -0
- package/src/redirects/middleware.ts +169 -0
- package/src/redirects/module.ts +124 -0
- package/src/redirects/pack.ts +89 -0
- package/src/redirects/removed-core.ts +151 -0
- package/src/redirects/removed.ts +293 -0
- package/src/redirects/rules.ts +355 -0
- package/src/redirects/slug-changes.ts +207 -0
- package/src/redirects/slug-core.ts +49 -0
- package/src/redirects/transfer.ts +92 -0
- package/src/reviews/lib.ts +624 -0
- package/src/reviews/pack.ts +135 -0
- package/src/reviews/schema.ts +128 -0
- package/src/reviews/settings.ts +26 -0
- package/src/robots/bots.ts +321 -0
- package/src/robots/data/bots.json +696 -0
- package/src/robots/data/verified.json +582 -0
- package/src/robots/directory.ts +26 -0
- package/src/robots/explain.ts +191 -0
- package/src/robots/guided.ts +300 -0
- package/src/robots/importer.ts +263 -0
- package/src/robots/middleware.ts +151 -0
- package/src/robots/module.ts +446 -0
- package/src/robots/pack.ts +69 -0
- package/src/robots/radar.ts +129 -0
- package/src/robots/rep.ts +362 -0
- package/src/robots/rules.ts +883 -0
- package/src/robots/transfer.ts +45 -0
- package/src/robots/validate.ts +524 -0
- package/src/schema/catalog.ts +146 -0
- package/src/schema/graph.ts +791 -0
- package/src/schema/module.ts +1138 -0
- package/src/schema/og-image.ts +100 -0
- package/src/schema/pack.ts +58 -0
- package/src/search/engine.ts +578 -0
- package/src/search/fallback.ts +80 -0
- package/src/search/live-cache.ts +116 -0
- package/src/search/live-client.ts +770 -0
- package/src/search/live-serve.ts +282 -0
- package/src/search/module.ts +261 -0
- package/src/search/pack.ts +57 -0
- package/src/search/query.ts +132 -0
- package/src/search/ratelimit-core.ts +118 -0
- package/src/search/ratelimit.ts +79 -0
- package/src/search/snippet.ts +135 -0
- package/src/search/title-index.ts +139 -0
- package/src/search/title-match.ts +192 -0
- package/src/shared.ts +71 -0
- package/src/version.ts +2 -0
- package/src/videos/date.ts +40 -0
- package/src/videos/engagement.ts +75 -0
- package/src/videos/lib.ts +679 -0
- package/src/videos/module.ts +1160 -0
- package/src/videos/pack.ts +187 -0
- package/src/videos/pause.ts +42 -0
- package/src/videos/poster.ts +324 -0
- package/src/videos/render.ts +336 -0
- package/src/videos/store.ts +551 -0
- package/src/videos/stream.ts +134 -0
- package/src/wpImport/convert.ts +702 -0
- package/src/wpImport/guests.ts +329 -0
- package/src/wpImport/gutenberg.ts +157 -0
- package/src/wpImport/markers.ts +66 -0
- package/src/wpImport/pack.ts +451 -0
- package/src/wpImport/parents.ts +202 -0
- package/src/wpImport/prepare.ts +592 -0
- package/src/wpImport/redirects.ts +358 -0
- package/src/wpImport/sources.ts +67 -0
- package/src/wpImport/stream.ts +160 -0
- package/src/wpImport/urls.ts +354 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Coywolf Video figure's site-wide look, shared by the block
|
|
3
|
+
* (src/astro/videos/CoywolfVideo.astro) and the live preview on the Videos
|
|
4
|
+
* settings tab, so the preview is what the site prints: the display settings
|
|
5
|
+
* (with their defaults and validation), the CSS, the custom properties a
|
|
6
|
+
* changed setting adds, the like icons, and the caption and
|
|
7
|
+
* like/views/date markup. Pure (imports only lib.ts and date.ts), so
|
|
8
|
+
* `node --test` can load it.
|
|
9
|
+
*/
|
|
10
|
+
import { type DateStyle, videoDateText } from "./date.js";
|
|
11
|
+
import { SHOW_DEFAULTS, type ShowDefaults, isHex } from "./lib.js";
|
|
12
|
+
|
|
13
|
+
export type LikeIcon = "heart" | "thumbs" | "star";
|
|
14
|
+
export type VideoScheme = "auto" | "light" | "dark" | "off";
|
|
15
|
+
export type VideoAlign = "left" | "center" | "right";
|
|
16
|
+
export const FONT_WEIGHTS = ["100", "200", "300", "400", "500", "600", "700", "800", "900"] as const;
|
|
17
|
+
|
|
18
|
+
/** Videos → Settings: Views & likes and Appearance (one stored object, videosDisplay). */
|
|
19
|
+
export interface VideoDisplay extends ShowDefaults {
|
|
20
|
+
dateStyle: DateStyle;
|
|
21
|
+
likeIcon: LikeIcon;
|
|
22
|
+
/** auto: follow the visitor's light/dark setting; light/dark: always; off: inherit the theme's text color. */
|
|
23
|
+
scheme: VideoScheme;
|
|
24
|
+
/** The name and description. */
|
|
25
|
+
align: VideoAlign;
|
|
26
|
+
/** The like, views and date row. */
|
|
27
|
+
metaAlign: VideoAlign;
|
|
28
|
+
/** Player corner radius in px. */
|
|
29
|
+
radius: number;
|
|
30
|
+
border: boolean;
|
|
31
|
+
borderWidth: number;
|
|
32
|
+
borderColor: string;
|
|
33
|
+
/** Hex colors; "" keeps the default. */
|
|
34
|
+
titleColor: string;
|
|
35
|
+
/** Name and description size, em. */
|
|
36
|
+
titleSize: number;
|
|
37
|
+
/** "" keeps the default (bold name, normal description). */
|
|
38
|
+
titleWeight: string;
|
|
39
|
+
descWeight: string;
|
|
40
|
+
likeColor: string;
|
|
41
|
+
likeBg: string;
|
|
42
|
+
likeActiveColor: string;
|
|
43
|
+
likeActiveBg: string;
|
|
44
|
+
metaColor: string;
|
|
45
|
+
/** Views and date size, em. */
|
|
46
|
+
metaSize: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The defaults are the block's look before these settings existed, so saving them changes nothing. */
|
|
50
|
+
export const DISPLAY_DEFAULTS: VideoDisplay = {
|
|
51
|
+
...SHOW_DEFAULTS,
|
|
52
|
+
dateStyle: "absolute",
|
|
53
|
+
likeIcon: "heart",
|
|
54
|
+
scheme: "auto",
|
|
55
|
+
align: "left",
|
|
56
|
+
metaAlign: "left",
|
|
57
|
+
radius: 8,
|
|
58
|
+
border: false,
|
|
59
|
+
borderWidth: 1,
|
|
60
|
+
borderColor: "#eeeeee",
|
|
61
|
+
titleColor: "",
|
|
62
|
+
titleSize: 0.95,
|
|
63
|
+
titleWeight: "",
|
|
64
|
+
descWeight: "",
|
|
65
|
+
likeColor: "",
|
|
66
|
+
likeBg: "",
|
|
67
|
+
likeActiveColor: "",
|
|
68
|
+
likeActiveBg: "",
|
|
69
|
+
metaColor: "",
|
|
70
|
+
metaSize: 0.875,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const pick = <T extends string>(value: unknown, allowed: readonly T[], fallback: T): T => (allowed.includes(value as T) ? (value as T) : fallback);
|
|
74
|
+
const flag = (value: unknown, fallback: boolean) => (typeof value === "boolean" ? value : fallback);
|
|
75
|
+
const hex = (value: unknown) => (isHex(value) ? value : "");
|
|
76
|
+
const range = (value: unknown, min: number, max: number, fallback: number) =>
|
|
77
|
+
typeof value === "number" && Number.isFinite(value) ? Math.min(max, Math.max(min, Math.round(value * 1000) / 1000)) : fallback;
|
|
78
|
+
|
|
79
|
+
/** A stored (or submitted) display object, with anything missing or invalid at its default. */
|
|
80
|
+
export function normalizeDisplay(raw: unknown): VideoDisplay {
|
|
81
|
+
const r = raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
|
|
82
|
+
const d = DISPLAY_DEFAULTS;
|
|
83
|
+
const align = ["left", "center", "right"] as const;
|
|
84
|
+
return {
|
|
85
|
+
showName: flag(r.showName, d.showName),
|
|
86
|
+
showDescription: flag(r.showDescription, d.showDescription),
|
|
87
|
+
showPlays: flag(r.showPlays, d.showPlays),
|
|
88
|
+
showLikes: flag(r.showLikes, d.showLikes),
|
|
89
|
+
showLikeCount: flag(r.showLikeCount, d.showLikeCount),
|
|
90
|
+
showDate: flag(r.showDate, d.showDate),
|
|
91
|
+
followSiteDefaults: flag(r.followSiteDefaults, d.followSiteDefaults),
|
|
92
|
+
dateStyle: pick(r.dateStyle, ["absolute", "relative"] as const, d.dateStyle),
|
|
93
|
+
likeIcon: pick(r.likeIcon, ["heart", "thumbs", "star"] as const, d.likeIcon),
|
|
94
|
+
scheme: pick(r.scheme, ["auto", "light", "dark", "off"] as const, d.scheme),
|
|
95
|
+
align: pick(r.align, align, d.align),
|
|
96
|
+
metaAlign: pick(r.metaAlign, align, d.metaAlign),
|
|
97
|
+
radius: Math.round(range(r.radius, 0, 48, d.radius)),
|
|
98
|
+
border: flag(r.border, d.border),
|
|
99
|
+
borderWidth: Math.round(range(r.borderWidth, 0, 20, d.borderWidth)),
|
|
100
|
+
borderColor: hex(r.borderColor) || d.borderColor,
|
|
101
|
+
titleColor: hex(r.titleColor),
|
|
102
|
+
titleSize: range(r.titleSize, 0.5, 4, d.titleSize),
|
|
103
|
+
titleWeight: pick(r.titleWeight, ["", ...FONT_WEIGHTS], ""),
|
|
104
|
+
descWeight: pick(r.descWeight, ["", ...FONT_WEIGHTS], ""),
|
|
105
|
+
likeColor: hex(r.likeColor),
|
|
106
|
+
likeBg: hex(r.likeBg),
|
|
107
|
+
likeActiveColor: hex(r.likeActiveColor),
|
|
108
|
+
likeActiveBg: hex(r.likeActiveBg),
|
|
109
|
+
metaColor: hex(r.metaColor),
|
|
110
|
+
metaSize: range(r.metaSize, 0.5, 4, d.metaSize),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The site's display settings with one block's own look applied: alignment,
|
|
116
|
+
* corner radius and border. Anything empty or invalid keeps the site's value
|
|
117
|
+
* (values are checked strictly, since they become CSS).
|
|
118
|
+
*/
|
|
119
|
+
export function blockDisplay(d: VideoDisplay, block: Record<string, unknown> | null | undefined): VideoDisplay {
|
|
120
|
+
if (!block) return d;
|
|
121
|
+
const out = { ...d };
|
|
122
|
+
const align = ["left", "center", "right"] as const;
|
|
123
|
+
if (align.includes(block.contentAlign as VideoAlign)) out.align = block.contentAlign as VideoAlign;
|
|
124
|
+
if (align.includes(block.metaAlign as VideoAlign)) out.metaAlign = block.metaAlign as VideoAlign;
|
|
125
|
+
const int = (v: unknown, min: number, max: number) => (typeof v === "number" && Number.isFinite(v) ? Math.min(max, Math.max(min, Math.round(v))) : null);
|
|
126
|
+
const radius = int(block.radius, 0, 48);
|
|
127
|
+
if (radius !== null) out.radius = radius;
|
|
128
|
+
const border = block.showBorder;
|
|
129
|
+
if (border === "show" || border === true) out.border = true;
|
|
130
|
+
else if (border === "hide" || border === false) out.border = false;
|
|
131
|
+
const width = int(block.borderWidth, 0, 20);
|
|
132
|
+
if (width !== null) out.borderWidth = width;
|
|
133
|
+
if (isHex(block.borderColor)) out.borderColor = block.borderColor;
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const JUSTIFY: Record<VideoAlign, string> = { left: "flex-start", center: "center", right: "flex-end" };
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Custom properties for the settings that differ from the defaults (nothing
|
|
141
|
+
* for a site that hasn't changed them, so its pages look as they did and
|
|
142
|
+
* theme CSS keeps working). `background` is the Player background color.
|
|
143
|
+
*/
|
|
144
|
+
export function displayVars(d: VideoDisplay, background?: string | null): string[] {
|
|
145
|
+
const D = DISPLAY_DEFAULTS;
|
|
146
|
+
const vars: string[] = [];
|
|
147
|
+
const add = (name: string, value: string) => vars.push(`--cw-video-${name}:${value}`);
|
|
148
|
+
if (isHex(background)) add("bg", background);
|
|
149
|
+
if (d.radius !== D.radius) add("radius", `${d.radius}px`);
|
|
150
|
+
if (d.border) add("border", `${d.borderWidth}px solid ${hex(d.borderColor) || D.borderColor}`);
|
|
151
|
+
if (d.titleColor) add("title-color", d.titleColor);
|
|
152
|
+
if (d.titleSize !== D.titleSize) add("title-size", `${d.titleSize}em`);
|
|
153
|
+
if (d.titleWeight) add("title-weight", d.titleWeight);
|
|
154
|
+
if (d.descWeight) add("desc-weight", d.descWeight);
|
|
155
|
+
if (d.align !== D.align) add("align", d.align);
|
|
156
|
+
if (d.metaAlign !== D.metaAlign) add("meta-justify", JUSTIFY[d.metaAlign]);
|
|
157
|
+
if (d.metaColor) add("muted", d.metaColor);
|
|
158
|
+
if (d.metaSize !== D.metaSize) add("meta-size", `${d.metaSize}em`);
|
|
159
|
+
if (d.likeColor) add("like-color", d.likeColor);
|
|
160
|
+
if (d.likeBg) add("like-bg", d.likeBg);
|
|
161
|
+
if (d.likeActiveColor) add("like-active", d.likeActiveColor);
|
|
162
|
+
if (d.likeActiveBg) add("like-active-bg", d.likeActiveBg);
|
|
163
|
+
return vars;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The figure's color-scheme class ("" for auto, the default). */
|
|
167
|
+
export const schemeClass = (scheme: VideoScheme) => (scheme === "auto" ? "" : `cw-video--${scheme}`);
|
|
168
|
+
|
|
169
|
+
// ── Markup ───────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
export const escapeHtml = (text: string) =>
|
|
172
|
+
String(text).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] as string);
|
|
173
|
+
|
|
174
|
+
/** Video Manager's like icons (Lucide), outlined; filled when liked. */
|
|
175
|
+
export const LIKE_ICON_PATHS: Record<LikeIcon, string[]> = {
|
|
176
|
+
heart: ["M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.51 4.04 3 5.5l7 7Z"],
|
|
177
|
+
thumbs: ["M7 10v12", "M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"],
|
|
178
|
+
star: [
|
|
179
|
+
"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z",
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export function likeIconSvg(icon: LikeIcon): string {
|
|
184
|
+
const paths = (LIKE_ICON_PATHS[icon] ?? LIKE_ICON_PATHS.heart).map((d) => `<path d="${d}" />`).join("");
|
|
185
|
+
return `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${paths}</svg>`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const number = new Intl.NumberFormat("en-US");
|
|
189
|
+
export const viewsText = (plays: number) => (plays > 0 ? `${number.format(plays)} ${plays === 1 ? "view" : "views"}` : "");
|
|
190
|
+
|
|
191
|
+
/** The name and description under the video ("" when neither shows). */
|
|
192
|
+
export function captionHtml(name: string, description: string, show: { name: boolean; description: boolean }): string {
|
|
193
|
+
const named = show.name && name !== "Video";
|
|
194
|
+
const described = show.description && Boolean(description);
|
|
195
|
+
if (!named && !described) return "";
|
|
196
|
+
const parts: string[] = [];
|
|
197
|
+
if (show.name) parts.push(`<strong class="cw-video__name">${escapeHtml(name)}</strong>`);
|
|
198
|
+
if (show.name && described) parts.push(" — ");
|
|
199
|
+
if (described) parts.push(`<span class="cw-video__desc">${escapeHtml(description)}</span>`);
|
|
200
|
+
return `<figcaption class="cw-video__caption">${parts.join("")}</figcaption>`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface MetaRow {
|
|
204
|
+
uid: string;
|
|
205
|
+
/** null: no like button. */
|
|
206
|
+
likes: number | null;
|
|
207
|
+
showLikeCount: boolean;
|
|
208
|
+
likeIcon: LikeIcon;
|
|
209
|
+
/** Render the button as liked (the settings preview; on the site the script sets it). */
|
|
210
|
+
liked?: boolean;
|
|
211
|
+
/** null: no views. */
|
|
212
|
+
plays: number | null;
|
|
213
|
+
/** The upload date (ISO), or null for none. */
|
|
214
|
+
date: string | null;
|
|
215
|
+
dateStyle: DateStyle;
|
|
216
|
+
now?: number;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** The like, views and date row ("" when there's nothing to show). */
|
|
220
|
+
export function metaHtml(m: MetaRow): string {
|
|
221
|
+
const parts: string[] = [];
|
|
222
|
+
if (m.likes !== null) {
|
|
223
|
+
const count = m.showLikeCount ? `<span class="cw-video__likes" data-cw-likes>${m.likes > 0 ? number.format(m.likes) : ""}</span>` : "";
|
|
224
|
+
parts.push(
|
|
225
|
+
`<button type="button" class="cw-video__like" data-cw-like="${escapeHtml(m.uid)}" aria-pressed="${m.liked ? "true" : "false"}">${likeIconSvg(m.likeIcon)}<span class="cw-video__sr">Like this video</span>${count}</button>`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
if (m.plays !== null) parts.push(`<span class="cw-video__plays" data-cw-plays>${viewsText(m.plays)}</span>`);
|
|
229
|
+
const dateText = videoDateText(m.date, m.dateStyle, m.now);
|
|
230
|
+
if (dateText) {
|
|
231
|
+
const relative = m.dateStyle === "relative" ? " data-cw-relative" : "";
|
|
232
|
+
parts.push(`<time class="cw-video__date" datetime="${escapeHtml(m.date as string)}"${relative}>${escapeHtml(dateText)}</time>`);
|
|
233
|
+
}
|
|
234
|
+
if (!parts.length) return "";
|
|
235
|
+
return `<div class="cw-video__meta">${parts.join("")}</div>`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── CSS ──────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Printed once per page. Settings arrive as custom properties on the figure
|
|
242
|
+
* (displayVars); a property that isn't set falls back to the look the block
|
|
243
|
+
* always had (and declarations without a fallback inherit, as before).
|
|
244
|
+
*/
|
|
245
|
+
export const VIDEO_CSS = `
|
|
246
|
+
.cw-video{--cw-video-bg:transparent;--cw-video-muted:#57606a;--cw-video-accent:#d1242f;margin-inline:auto;margin-block:1.5em}
|
|
247
|
+
@media (prefers-color-scheme:dark){.cw-video:not(.cw-video--light):not(.cw-video--off){--cw-video-muted:#9198a1;--cw-video-accent:#ff7b72}}
|
|
248
|
+
.cw-video.cw-video--dark{--cw-video-muted:#9198a1;--cw-video-accent:#ff7b72}
|
|
249
|
+
.cw-video.cw-video--off{--cw-video-muted:currentColor;--cw-video-accent:currentColor}
|
|
250
|
+
.cw-video .cw-video__frame{position:relative;aspect-ratio:var(--cw-video-ratio,16 / 9);background-color:var(--cw-video-bg);border:var(--cw-video-border,0);border-radius:var(--cw-video-radius,0.5rem);overflow:hidden}
|
|
251
|
+
.cw-video .cw-video__frame iframe{position:absolute;inset:0;width:100%;height:100%;border:0}
|
|
252
|
+
.cw-video .cw-video__poster{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
|
|
253
|
+
.cw-video .cw-video__play{position:absolute;inset:50% auto auto 50%;translate:-50% -50%;display:grid;place-items:center;width:4rem;height:4rem;padding:0;padding-left:0.2rem;border:0;border-radius:50%;background:rgb(0 0 0 / 0.65);color:#fff;cursor:pointer;transition:background 0.15s}
|
|
254
|
+
.cw-video .cw-video__play:hover{background:var(--cw-video-accent)}
|
|
255
|
+
.cw-video .cw-video__play:focus-visible{outline:3px solid #fff;outline-offset:2px}
|
|
256
|
+
.cw-video .cw-video__play[hidden]{display:none}
|
|
257
|
+
.cw-video .cw-video__toggle{position:absolute;inset:auto auto 0.5rem 0.5rem;z-index:1;display:grid;place-items:center;width:2.75rem;height:2.75rem;padding:0;border:0;border-radius:50%;background:rgb(0 0 0 / 0.65);color:#fff;cursor:pointer;transition:background 0.15s}
|
|
258
|
+
.cw-video .cw-video__toggle:hover{background:var(--cw-video-accent)}
|
|
259
|
+
.cw-video .cw-video__toggle:focus-visible{outline:3px solid #fff;outline-offset:2px;box-shadow:0 0 0 5px #000}
|
|
260
|
+
.cw-video .cw-video__toggle[data-paused] .cw-video__pause,.cw-video .cw-video__toggle:not([data-paused]) .cw-video__resume{display:none}
|
|
261
|
+
.cw-video.cw-video--gif .cw-video__frame{border-radius:var(--cw-video-radius,0)}
|
|
262
|
+
.cw-video .cw-video__caption{margin-top:0.5em;font-size:var(--cw-video-title-size,0.95em);color:var(--cw-video-title-color);text-align:var(--cw-video-align)}
|
|
263
|
+
.cw-video .cw-video__name{font-weight:var(--cw-video-title-weight,bolder)}
|
|
264
|
+
.cw-video .cw-video__desc{font-weight:var(--cw-video-desc-weight)}
|
|
265
|
+
.cw-video .cw-video__meta{display:flex;flex-wrap:wrap;align-items:center;justify-content:var(--cw-video-meta-justify);gap:0.75em;margin-top:0.5em;font-size:var(--cw-video-meta-size,0.875em);color:var(--cw-video-muted)}
|
|
266
|
+
.cw-video .cw-video__plays:empty,.cw-video .cw-video__likes:empty{display:none}
|
|
267
|
+
.cw-video .cw-video__like{display:inline-flex;align-items:center;gap:0.35em;min-height:2rem;padding:0.25em 0.75em;border:1px solid currentColor;border-radius:999px;background:var(--cw-video-like-bg,transparent);color:var(--cw-video-like-color);font:inherit;cursor:pointer;transition:color 0.15s,background-color 0.15s}
|
|
268
|
+
.cw-video .cw-video__like svg{transition:fill 0.15s}
|
|
269
|
+
.cw-video .cw-video__like[aria-pressed="true"]{color:var(--cw-video-like-active,var(--cw-video-accent));background:var(--cw-video-like-active-bg,var(--cw-video-like-bg,transparent))}
|
|
270
|
+
.cw-video .cw-video__like[aria-pressed="true"] svg{fill:currentColor}
|
|
271
|
+
@media (hover:hover){
|
|
272
|
+
.cw-video .cw-video__like:not([data-held]):hover{color:var(--cw-video-like-color);background:var(--cw-video-like-bg,transparent)}
|
|
273
|
+
.cw-video .cw-video__like:not([data-held]):hover svg{fill:currentColor}
|
|
274
|
+
}
|
|
275
|
+
.cw-video .cw-video__like[data-pop] svg{animation:cw-video-pop 0.35s ease}
|
|
276
|
+
@keyframes cw-video-pop{0%{transform:scale(1)}40%{transform:scale(1.35) rotate(-8deg)}100%{transform:scale(1)}}
|
|
277
|
+
@media (prefers-reduced-motion:reduce){.cw-video .cw-video__like[data-pop] svg{animation:none}.cw-video .cw-video__like,.cw-video .cw-video__like svg{transition:none}}
|
|
278
|
+
.cw-video .cw-video__like:focus-visible{outline:2px solid var(--cw-video-accent);outline-offset:2px}
|
|
279
|
+
.cw-video .cw-video__sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
|
|
280
|
+
`.trim();
|
|
281
|
+
|
|
282
|
+
// ── Settings preview ─────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
/** The preview's own frame: a checkerboard when the background is transparent, and a stage for the light/dark schemes. */
|
|
285
|
+
export const VIDEO_PREVIEW_CSS = `
|
|
286
|
+
.cw-video-stage{padding:4px 0}
|
|
287
|
+
.cw-video-stage--light{background:#fff;color:#1f2328;margin:-16px;padding:16px}
|
|
288
|
+
.cw-video-stage--dark{background:#0f0f0f;color:#f0f6fc;margin:-16px;padding:16px}
|
|
289
|
+
.cw-video-stage .cw-video{margin-block:0}
|
|
290
|
+
.cw-video .cw-video__frame--checker{background-image:conic-gradient(#e5e5e5 90deg,#fafafa 90deg 180deg,#e5e5e5 180deg 270deg,#fafafa 270deg);background-size:16px 16px}
|
|
291
|
+
.cw-video .cw-video__frame span.cw-video__play{cursor:default}
|
|
292
|
+
.cw-video-stage__label{margin:1.25em 0 0;font-size:0.75em;opacity:0.7}
|
|
293
|
+
`.trim();
|
|
294
|
+
|
|
295
|
+
const SAMPLE_UID = "0".repeat(32);
|
|
296
|
+
const PLAY_ICON = '<svg viewBox="0 0 24 24" width="28" height="28" aria-hidden="true" focusable="false"><path d="M8 5v14l11-7z" fill="currentColor" /></svg>';
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* A sample video with every part the settings can show: the frame (16:9),
|
|
300
|
+
* name and description, like button (175), "23 views", an upload date seven
|
|
301
|
+
* months back. The preview frame runs no scripts, so a
|
|
302
|
+
* second like button shows the clicked state; hover works as on the site.
|
|
303
|
+
*/
|
|
304
|
+
export function renderVideoPreviewHtml(d: VideoDisplay, background: string | null = null, now = Date.now()): string {
|
|
305
|
+
const vars = displayVars(d, background);
|
|
306
|
+
const cls = ["cw-video", schemeClass(d.scheme)].filter(Boolean).join(" ");
|
|
307
|
+
const style = ["--cw-video-ratio:16 / 9", ...vars].join(";");
|
|
308
|
+
const checker = isHex(background) ? "" : " cw-video__frame--checker";
|
|
309
|
+
const date = new Date(now - 213 * 86_400_000).toISOString();
|
|
310
|
+
const row = (liked: boolean) =>
|
|
311
|
+
metaHtml({
|
|
312
|
+
uid: SAMPLE_UID,
|
|
313
|
+
likes: d.showLikes ? 175 : null,
|
|
314
|
+
showLikeCount: d.showLikeCount,
|
|
315
|
+
likeIcon: d.likeIcon,
|
|
316
|
+
liked,
|
|
317
|
+
plays: d.showPlays && !liked ? 23 : null,
|
|
318
|
+
date: d.showDate && !liked ? date : null,
|
|
319
|
+
dateStyle: d.dateStyle,
|
|
320
|
+
now,
|
|
321
|
+
});
|
|
322
|
+
const figure = `<figure class="${cls}" style="${escapeHtml(style)}"><div class="cw-video__frame${checker}"><span class="cw-video__play" aria-hidden="true">${PLAY_ICON}</span></div>${captionHtml(
|
|
323
|
+
"Your video name",
|
|
324
|
+
"A short video description.",
|
|
325
|
+
{ name: d.showName, description: d.showDescription },
|
|
326
|
+
)}${row(false)}</figure>`;
|
|
327
|
+
const liked = d.showLikes
|
|
328
|
+
? `<p class="cw-video-stage__label">After clicking like:</p><div class="${cls}" style="${escapeHtml(vars.join(";"))}">${row(true)}</div>`
|
|
329
|
+
: "";
|
|
330
|
+
const stage = d.scheme === "light" || d.scheme === "dark" ? ` cw-video-stage--${d.scheme}` : "";
|
|
331
|
+
return `<div class="cw-video-stage${stage}">${figure}${liked}</div>`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Re-exported for the Astro component and the admin page. */
|
|
335
|
+
export type { DateStyle } from "./date.js";
|
|
336
|
+
export type { ShowDefaults } from "./lib.js";
|