@coywolf/emdash 0.0.0-stage → 0.35.13

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.
Files changed (261) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -2
  3. package/package.json +70 -5
  4. package/src/admin/admin-css.ts +57 -0
  5. package/src/admin/ai.tsx +915 -0
  6. package/src/admin/backups.tsx +613 -0
  7. package/src/admin/breadcrumbs.tsx +542 -0
  8. package/src/admin/codeBlocks.tsx +204 -0
  9. package/src/admin/customBlocks.tsx +321 -0
  10. package/src/admin/dirty.ts +20 -0
  11. package/src/admin/discovery.tsx +463 -0
  12. package/src/admin/download.ts +28 -0
  13. package/src/admin/feature-switches.tsx +124 -0
  14. package/src/admin/features.tsx +142 -0
  15. package/src/admin/files.tsx +791 -0
  16. package/src/admin/formUploads.tsx +445 -0
  17. package/src/admin/guides.tsx +18 -0
  18. package/src/admin/headings.tsx +262 -0
  19. package/src/admin/images.tsx +825 -0
  20. package/src/admin/index.tsx +62 -0
  21. package/src/admin/links.tsx +980 -0
  22. package/src/admin/performance.tsx +330 -0
  23. package/src/admin/preview.tsx +63 -0
  24. package/src/admin/redirects-removed.tsx +266 -0
  25. package/src/admin/redirects.tsx +470 -0
  26. package/src/admin/reviews.tsx +232 -0
  27. package/src/admin/robots-bots.tsx +647 -0
  28. package/src/admin/robots-shared.tsx +109 -0
  29. package/src/admin/robots-wizard.tsx +748 -0
  30. package/src/admin/robots.tsx +911 -0
  31. package/src/admin/save-bar.tsx +134 -0
  32. package/src/admin/schema.tsx +1187 -0
  33. package/src/admin/search.tsx +355 -0
  34. package/src/admin/settings-ui.tsx +87 -0
  35. package/src/admin/trash-prompt-core.ts +135 -0
  36. package/src/admin/trash-prompt.tsx +156 -0
  37. package/src/admin/usage.tsx +301 -0
  38. package/src/admin/videos-display.tsx +248 -0
  39. package/src/admin/videos.tsx +1202 -0
  40. package/src/admin/wpImport.tsx +1516 -0
  41. package/src/ai/entities.ts +59 -0
  42. package/src/ai/export.ts +61 -0
  43. package/src/ai/logic.test.ts +187 -0
  44. package/src/ai/logic.ts +606 -0
  45. package/src/ai/pack.ts +121 -0
  46. package/src/ai/prompts.ts +87 -0
  47. package/src/ai/providers.ts +276 -0
  48. package/src/ai/queue.test.ts +97 -0
  49. package/src/ai/routes.ts +359 -0
  50. package/src/ai/store.ts +301 -0
  51. package/src/ai/wikidata.test.ts +184 -0
  52. package/src/ai/wikidata.ts +87 -0
  53. package/src/ai/worker.ts +530 -0
  54. package/src/astro/astro-modules.d.ts +6 -0
  55. package/src/astro/breadcrumbs/Breadcrumbs.astro +129 -0
  56. package/src/astro/breadcrumbs/BreadcrumbsBlock.astro +12 -0
  57. package/src/astro/codeBlocks/CodeBlock.astro +62 -0
  58. package/src/astro/codeBlocks/EmDashCode.astro +61 -0
  59. package/src/astro/customBlocks/CustomBlock.astro +64 -0
  60. package/src/astro/customBlocks/Details.astro +8 -0
  61. package/src/astro/customBlocks/Disclosure.astro +8 -0
  62. package/src/astro/customBlocks/Note.astro +8 -0
  63. package/src/astro/customBlocks/Podcast.astro +8 -0
  64. package/src/astro/customBlocks/Quote.astro +8 -0
  65. package/src/astro/customBlocks/Testimonial.astro +8 -0
  66. package/src/astro/files/FileCard.astro +103 -0
  67. package/src/astro/headings/HeadingBlock.astro +129 -0
  68. package/src/astro/headings/TableOfContents.astro +45 -0
  69. package/src/astro/headings/request.ts +19 -0
  70. package/src/astro/images/ResponsiveImage.astro +60 -0
  71. package/src/astro/index.ts +84 -0
  72. package/src/astro/reviews/CoywolfReview.astro +27 -0
  73. package/src/astro/reviews/Review.astro +49 -0
  74. package/src/astro/search/SearchBox.astro +61 -0
  75. package/src/astro/search/SearchBoxForm.astro +509 -0
  76. package/src/astro/tables/CaptionedTable.astro +72 -0
  77. package/src/astro/videos/CoywolfVideo.astro +448 -0
  78. package/src/backups/download.ts +112 -0
  79. package/src/backups/dump.test.mjs +287 -0
  80. package/src/backups/dump.ts +288 -0
  81. package/src/backups/module.ts +276 -0
  82. package/src/backups/pack.ts +33 -0
  83. package/src/backups/restore.test.mjs +103 -0
  84. package/src/backups/restore.ts +209 -0
  85. package/src/backups/store.ts +409 -0
  86. package/src/breadcrumbs/pack.ts +113 -0
  87. package/src/breadcrumbs/settings.ts +102 -0
  88. package/src/breadcrumbs/stamp.ts +40 -0
  89. package/src/breadcrumbs/trail.ts +123 -0
  90. package/src/codeBlocks/contrast.ts +179 -0
  91. package/src/codeBlocks/hljs.d.ts +5 -0
  92. package/src/codeBlocks/pack.ts +110 -0
  93. package/src/codeBlocks/render.test.ts +152 -0
  94. package/src/codeBlocks/render.ts +304 -0
  95. package/src/codeBlocks/settings.ts +18 -0
  96. package/src/codeBlocks/themes.generated.ts +304 -0
  97. package/src/codeBlocks/themes.ts +114 -0
  98. package/src/core/compose.ts +244 -0
  99. package/src/core/content-url.ts +888 -0
  100. package/src/core/csv.ts +119 -0
  101. package/src/core/d1-batch.ts +65 -0
  102. package/src/core/entry-docs.ts +175 -0
  103. package/src/core/features-module.ts +48 -0
  104. package/src/core/features.ts +392 -0
  105. package/src/core/generation.ts +88 -0
  106. package/src/core/guide-content.ts +166 -0
  107. package/src/core/module.ts +104 -0
  108. package/src/core/read-failures.ts +57 -0
  109. package/src/core/request-memo.ts +71 -0
  110. package/src/core/secrets.ts +83 -0
  111. package/src/core/settings-enhance.ts +74 -0
  112. package/src/core/site.ts +36 -0
  113. package/src/core/storage.ts +27 -0
  114. package/src/customBlocks/pack.ts +260 -0
  115. package/src/customBlocks/render.ts +424 -0
  116. package/src/customBlocks/rich.ts +313 -0
  117. package/src/customBlocks/settings.ts +46 -0
  118. package/src/discovery/build.ts +252 -0
  119. package/src/discovery/feed-build.ts +217 -0
  120. package/src/discovery/feed.ts +172 -0
  121. package/src/discovery/indexnow.ts +114 -0
  122. package/src/discovery/llms.ts +79 -0
  123. package/src/discovery/markdown.ts +294 -0
  124. package/src/discovery/memory-cache.ts +97 -0
  125. package/src/discovery/middleware.ts +186 -0
  126. package/src/discovery/module.ts +353 -0
  127. package/src/discovery/news.ts +99 -0
  128. package/src/discovery/pack.ts +58 -0
  129. package/src/discovery/settings.ts +91 -0
  130. package/src/discovery/urls.ts +35 -0
  131. package/src/env.d.ts +10 -0
  132. package/src/files/card.ts +231 -0
  133. package/src/files/counts.test.ts +114 -0
  134. package/src/files/counts.ts +60 -0
  135. package/src/files/export.ts +44 -0
  136. package/src/files/format.test.ts +93 -0
  137. package/src/files/format.ts +163 -0
  138. package/src/files/module.ts +596 -0
  139. package/src/files/pack.ts +92 -0
  140. package/src/files/r2-rules.test.ts +30 -0
  141. package/src/files/r2-rules.ts +41 -0
  142. package/src/files/range.ts +50 -0
  143. package/src/files/s3.ts +114 -0
  144. package/src/files/serve.ts +141 -0
  145. package/src/files/sigv4.test.ts +64 -0
  146. package/src/files/sigv4.ts +160 -0
  147. package/src/files/site.ts +137 -0
  148. package/src/files/walker.ts +58 -0
  149. package/src/formUploads/lib.ts +266 -0
  150. package/src/formUploads/pack.ts +55 -0
  151. package/src/formUploads/wrap.ts +592 -0
  152. package/src/forms.ts +16 -0
  153. package/src/headings/headings.test.ts +159 -0
  154. package/src/headings/pack.ts +154 -0
  155. package/src/headings/render.ts +139 -0
  156. package/src/headings/settings.ts +117 -0
  157. package/src/headings/slug.ts +68 -0
  158. package/src/headings/stamp.ts +142 -0
  159. package/src/headings/test-hooks.mjs +14 -0
  160. package/src/headings/toc.ts +48 -0
  161. package/src/images/cloudflare.ts +329 -0
  162. package/src/images/lib.ts +221 -0
  163. package/src/images/module.ts +372 -0
  164. package/src/images/pack.ts +804 -0
  165. package/src/images/pending.ts +37 -0
  166. package/src/images/settings.ts +48 -0
  167. package/src/images/setup.ts +192 -0
  168. package/src/images/variants-estimate.ts +162 -0
  169. package/src/images/variants-store.ts +475 -0
  170. package/src/images/variants.ts +311 -0
  171. package/src/index.ts +143 -0
  172. package/src/links/check.ts +101 -0
  173. package/src/links/classify.ts +270 -0
  174. package/src/links/export.ts +59 -0
  175. package/src/links/module.ts +652 -0
  176. package/src/links/pack.ts +43 -0
  177. package/src/links/pt.ts +325 -0
  178. package/src/links/store.ts +704 -0
  179. package/src/middleware.ts +287 -0
  180. package/src/modules.ts +53 -0
  181. package/src/options.ts +66 -0
  182. package/src/pageCache/lib.test.mjs +224 -0
  183. package/src/pageCache/lib.ts +274 -0
  184. package/src/pageCache/pack.ts +240 -0
  185. package/src/pageCache/usage-core.ts +152 -0
  186. package/src/pageCache/usage.ts +262 -0
  187. package/src/pageCache/warm.test.mjs +670 -0
  188. package/src/pageCache/warm.ts +722 -0
  189. package/src/redirects/middleware.ts +169 -0
  190. package/src/redirects/module.ts +124 -0
  191. package/src/redirects/pack.ts +89 -0
  192. package/src/redirects/removed-core.ts +151 -0
  193. package/src/redirects/removed.ts +293 -0
  194. package/src/redirects/rules.ts +355 -0
  195. package/src/redirects/slug-changes.ts +207 -0
  196. package/src/redirects/slug-core.test.ts +50 -0
  197. package/src/redirects/slug-core.ts +49 -0
  198. package/src/redirects/transfer.ts +92 -0
  199. package/src/reviews/lib.ts +624 -0
  200. package/src/reviews/pack.ts +135 -0
  201. package/src/reviews/schema.ts +128 -0
  202. package/src/reviews/settings.ts +26 -0
  203. package/src/robots/bots.ts +321 -0
  204. package/src/robots/data/bots.json +696 -0
  205. package/src/robots/data/verified.json +582 -0
  206. package/src/robots/directory.ts +26 -0
  207. package/src/robots/explain.ts +191 -0
  208. package/src/robots/guided.ts +300 -0
  209. package/src/robots/importer.ts +263 -0
  210. package/src/robots/middleware.ts +151 -0
  211. package/src/robots/module.ts +446 -0
  212. package/src/robots/pack.ts +69 -0
  213. package/src/robots/radar.ts +129 -0
  214. package/src/robots/rep.test.ts +335 -0
  215. package/src/robots/rep.ts +362 -0
  216. package/src/robots/rules.test.ts +251 -0
  217. package/src/robots/rules.ts +883 -0
  218. package/src/robots/transfer.ts +45 -0
  219. package/src/robots/validate.ts +524 -0
  220. package/src/schema/catalog.ts +146 -0
  221. package/src/schema/graph.test.mjs +522 -0
  222. package/src/schema/graph.ts +791 -0
  223. package/src/schema/module.ts +1138 -0
  224. package/src/schema/og-image.ts +100 -0
  225. package/src/schema/pack.ts +58 -0
  226. package/src/search/engine.ts +578 -0
  227. package/src/search/fallback.ts +80 -0
  228. package/src/search/live-cache.ts +116 -0
  229. package/src/search/live-client.ts +770 -0
  230. package/src/search/live-serve.ts +282 -0
  231. package/src/search/module.ts +261 -0
  232. package/src/search/pack.ts +57 -0
  233. package/src/search/query.ts +132 -0
  234. package/src/search/ratelimit-core.ts +118 -0
  235. package/src/search/ratelimit.ts +79 -0
  236. package/src/search/snippet.ts +135 -0
  237. package/src/search/title-index.ts +139 -0
  238. package/src/search/title-match.ts +192 -0
  239. package/src/shared.ts +71 -0
  240. package/src/version.ts +2 -0
  241. package/src/videos/date.ts +40 -0
  242. package/src/videos/engagement.ts +75 -0
  243. package/src/videos/lib.ts +679 -0
  244. package/src/videos/module.ts +1160 -0
  245. package/src/videos/pack.ts +187 -0
  246. package/src/videos/pause.ts +42 -0
  247. package/src/videos/poster.ts +324 -0
  248. package/src/videos/render.ts +336 -0
  249. package/src/videos/store.ts +551 -0
  250. package/src/videos/stream.ts +134 -0
  251. package/src/wpImport/convert.ts +702 -0
  252. package/src/wpImport/guests.ts +329 -0
  253. package/src/wpImport/gutenberg.ts +157 -0
  254. package/src/wpImport/markers.ts +66 -0
  255. package/src/wpImport/pack.ts +451 -0
  256. package/src/wpImport/parents.ts +202 -0
  257. package/src/wpImport/prepare.ts +592 -0
  258. package/src/wpImport/redirects.ts +358 -0
  259. package/src/wpImport/sources.ts +67 -0
  260. package/src/wpImport/stream.ts +160 -0
  261. package/src/wpImport/urls.ts +354 -0
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Reviews page: the review box's accent color and custom CSS, with a live
3
+ * preview of a sample review (rendered by the same code as the site).
4
+ * Turning Reviews on or off happens on the Features page.
5
+ */
6
+ import { Banner, Button, Input, InputArea, Loader } from "@cloudflare/kumo";
7
+ import { ArrowCounterClockwise, FloppyDisk } from "@phosphor-icons/react";
8
+ import { apiFetch, parseApiResponse } from "emdash/plugin-utils";
9
+ import * as React from "react";
10
+
11
+ import { SaveBar } from "./save-bar.js";
12
+ import { DEFAULT_ACCENT, MAX_CUSTOM_CSS, SAMPLE_REVIEW, isAccent, pageCss, renderReviewHtml, sanitizeCustomCss } from "../reviews/lib.js";
13
+ import { PartSwitches } from "./feature-switches.js";
14
+
15
+ const API = "/_emdash/api/plugins/coywolf-pack";
16
+
17
+ interface Style {
18
+ accent: string;
19
+ css: string;
20
+ }
21
+
22
+
23
+ const EXAMPLE_CSS = `/* Target .cw-review … so rules only reach review boxes. */
24
+ .cw-review {
25
+ --cw-review-accent: #b22d47;
26
+ --cw-review-column-min: 100%; /* one column */
27
+ }`;
28
+
29
+ const errorText = (cause: unknown, fallback: string) => (cause instanceof Error && cause.message ? cause.message : fallback);
30
+
31
+ async function post<T>(path: string, body: unknown, fallback: string): Promise<T> {
32
+ const response = await apiFetch(`${API}/${path}`, {
33
+ method: "POST",
34
+ headers: { "Content-Type": "application/json" },
35
+ body: JSON.stringify(body),
36
+ });
37
+ return parseApiResponse<T>(response, fallback);
38
+ }
39
+
40
+ /** The sample review in an isolated frame, so custom CSS can't restyle this admin page. */
41
+ function Preview({ style, width }: { style: Style; width: "wide" | "phone" }) {
42
+ const frame = React.useRef<HTMLIFrameElement>(null);
43
+ const [height, setHeight] = React.useState(320);
44
+ const doc = React.useMemo(() => {
45
+ const css = pageCss({ accent: isAccent(style.accent) ? style.accent : DEFAULT_ACCENT, css: sanitizeCustomCss(style.css) });
46
+ return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>html{color-scheme:light dark}body{margin:0;padding:16px;font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;line-height:1.5;background:Canvas;color:CanvasText}</style><style>${css}</style></head><body>${renderReviewHtml(SAMPLE_REVIEW)}</body></html>`;
47
+ }, [style.accent, style.css]);
48
+ const measure = () => {
49
+ const body = frame.current?.contentDocument?.documentElement;
50
+ if (body) setHeight(Math.min(1200, Math.max(120, body.scrollHeight)));
51
+ };
52
+ return (
53
+ <iframe
54
+ ref={frame}
55
+ title="Review box preview"
56
+ // No scripts; same origin only so the frame's height can be measured.
57
+ sandbox="allow-same-origin"
58
+ srcDoc={doc}
59
+ onLoad={measure}
60
+ className="block rounded-lg border border-kumo-line bg-white"
61
+ style={{ width: width === "phone" ? 375 : "100%", maxWidth: "100%", height }}
62
+ />
63
+ );
64
+ }
65
+
66
+ export function ReviewsPage() {
67
+ const [saved, setSaved] = React.useState<{ style: Style } | null>(null);
68
+ const [style, setStyle] = React.useState<Style>({ accent: DEFAULT_ACCENT, css: "" });
69
+ const [width, setWidth] = React.useState<"wide" | "phone">("wide");
70
+ const [error, setError] = React.useState<string | null>(null);
71
+ const [status, setStatus] = React.useState("");
72
+ const [saving, setSaving] = React.useState(false);
73
+
74
+ React.useEffect(() => {
75
+ void (async () => {
76
+ try {
77
+ const settings = await apiFetch(`${API}/reviews/settings`).then((r) => parseApiResponse<Style>(r, "Couldn't load settings"));
78
+ const loaded = { accent: settings.accent, css: settings.css };
79
+ setStyle(loaded);
80
+ setSaved({ style: loaded });
81
+ } catch (cause) {
82
+ setError(errorText(cause, "Couldn't load settings"));
83
+ }
84
+ })();
85
+ }, []);
86
+
87
+ const styleDirty = saved !== null && (style.accent !== saved.style.accent || style.css !== saved.style.css);
88
+ const dirty = styleDirty;
89
+ const accentValid = isAccent(style.accent);
90
+ const tooLong = style.css.length > MAX_CUSTOM_CSS;
91
+
92
+ async function save() {
93
+ if (!saved || !accentValid || tooLong) return;
94
+ setSaving(true);
95
+ setError(null);
96
+ setStatus("Saving…");
97
+ try {
98
+ let next = style;
99
+ if (styleDirty) next = await post<Style>("reviews/save", style, "Couldn't save the style");
100
+ setStyle(next);
101
+ setSaved({ style: next });
102
+ setStatus("Saved. The site picks up changes within a minute.");
103
+ } catch (cause) {
104
+ setStatus("");
105
+ setError(errorText(cause, "Couldn't save"));
106
+ } finally {
107
+ setSaving(false);
108
+ }
109
+ }
110
+
111
+ return (
112
+ <div className="space-y-6">
113
+ <header className="grid min-w-0 gap-4 border-b border-kumo-line pb-4">
114
+ <div className="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-x-3 gap-y-1">
115
+ <h1 className="flex min-h-9 min-w-0 items-center text-2xl font-semibold leading-tight">Reviews</h1>
116
+ <div className="flex shrink-0 justify-end">
117
+ <Button variant="primary" icon={<FloppyDisk />} disabled={!dirty || saving || !accentValid || tooLong} onClick={() => void save()}>
118
+ Save
119
+ </Button>
120
+ </div>
121
+ <p className="col-span-2 text-sm leading-5 text-pretty text-kumo-subtle">
122
+ Add a review to an entry with the Coywolf Review block: a rating badge, what you liked most, and what could be better.
123
+ The box is styled with plain CSS sent only on pages with a review; no script.
124
+ </p>
125
+ </div>
126
+ </header>
127
+
128
+ <PartSwitches module="reviews" />
129
+
130
+ {error && <Banner variant="error" role="alert" description={error} />}
131
+ <p className="sr-only" role="status" aria-live="polite">
132
+ {status}
133
+ </p>
134
+ {status && !saving && <p className="text-sm text-kumo-subtle">{status}</p>}
135
+
136
+ {!saved && !error && (
137
+ <div className="flex justify-center py-12">
138
+ <Loader />
139
+ </div>
140
+ )}
141
+
142
+ {saved && (
143
+ <div className="grid gap-6 cw-lg-cols-24-fill">
144
+ <div className="space-y-6">
145
+ <div className="space-y-2">
146
+ <div className="flex items-end gap-2">
147
+ <input
148
+ type="color"
149
+ aria-label="Pick the accent color"
150
+ value={accentValid && style.accent.length === 7 ? style.accent : DEFAULT_ACCENT}
151
+ onChange={(e) => setStyle((cur) => ({ ...cur, accent: e.target.value }))}
152
+ className="h-9 w-12 shrink-0 cursor-pointer rounded border border-kumo-line bg-transparent p-0.5"
153
+ />
154
+ <div className="min-w-0 flex-1">
155
+ <Input
156
+ label="Accent color (rating badge)"
157
+ value={style.accent}
158
+ aria-invalid={!accentValid}
159
+ aria-describedby={accentValid ? undefined : "cw-reviews-accent-error"}
160
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => setStyle((cur) => ({ ...cur, accent: e.target.value.trim() }))}
161
+ />
162
+ </div>
163
+ <Button
164
+ variant="secondary"
165
+ icon={<ArrowCounterClockwise />}
166
+ disabled={style.accent === DEFAULT_ACCENT}
167
+ onClick={() => setStyle((cur) => ({ ...cur, accent: DEFAULT_ACCENT }))}
168
+ >
169
+ Default
170
+ </Button>
171
+ </div>
172
+ {!accentValid && (
173
+ <p id="cw-reviews-accent-error" className="text-sm text-kumo-danger" role="alert">
174
+ Use a hex color like {DEFAULT_ACCENT}.
175
+ </p>
176
+ )}
177
+ <p className="text-xs text-kumo-subtle">Badge text is white; pick a color dark enough to read it.</p>
178
+ </div>
179
+
180
+ <div className="space-y-2">
181
+ <InputArea
182
+ label="Custom CSS"
183
+ rows={10}
184
+ value={style.css}
185
+ placeholder={EXAMPLE_CSS}
186
+ spellCheck={false}
187
+ className="font-mono text-xs"
188
+ aria-invalid={tooLong || undefined}
189
+ aria-describedby="cw-reviews-css-limit"
190
+ onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setStyle((cur) => ({ ...cur, css: e.target.value }))}
191
+ />
192
+ <p id="cw-reviews-css-limit" className={`text-xs ${tooLong ? "text-kumo-danger" : "text-kumo-subtle"}`}>
193
+ {style.css.length.toLocaleString()} / {MAX_CUSTOM_CSS.toLocaleString()} characters. Added after the built-in styles on pages
194
+ with a review. Start rules with <code>.cw-review</code> and set the <code>--cw-review-*</code> properties (see docs/reviews.md in the Coywolf Pack repository) to
195
+ change colors and layout.
196
+ </p>
197
+ </div>
198
+ </div>
199
+
200
+ <section aria-labelledby="cw-review-preview-title" className="min-w-0">
201
+ <div className="mb-2 flex items-center justify-between gap-2">
202
+ <h2 id="cw-review-preview-title" className="text-sm font-medium">
203
+ Preview
204
+ </h2>
205
+ <div className="flex gap-1" role="group" aria-label="Preview width">
206
+ <Button size="sm" variant={width === "wide" ? "primary" : "secondary"} aria-pressed={width === "wide"} onClick={() => setWidth("wide")}>
207
+ Wide
208
+ </Button>
209
+ <Button size="sm" variant={width === "phone" ? "primary" : "secondary"} aria-pressed={width === "phone"} onClick={() => setWidth("phone")}>
210
+ Phone
211
+ </Button>
212
+ </div>
213
+ </div>
214
+ <Preview style={style} width={width} />
215
+ <p className="mt-2 text-xs text-kumo-subtle">
216
+ The preview uses this page's fonts and your computer's light or dark setting. On the site the box uses your theme's fonts
217
+ and text color, and turns dark only if the theme supports dark mode (its color-scheme).
218
+ </p>
219
+ </section>
220
+ </div>
221
+ )}
222
+
223
+ <SaveBar
224
+ dirty={dirty}
225
+ saving={saving}
226
+ canSave={accentValid && !tooLong}
227
+ onSave={() => void save()}
228
+ onDiscard={() => saved && setStyle(saved.style)}
229
+ />
230
+ </div>
231
+ );
232
+ }