daily-soup-widget 0.2.2 → 0.3.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/README.md +21 -4
- package/dist/embed.cjs.js +220 -158
- package/dist/embed.cjs.js.map +4 -4
- package/dist/embed.esm.js +219 -157
- package/dist/embed.esm.js.map +4 -4
- package/dist/embed.js +67 -25
- package/dist/embed.js.map +4 -4
- package/dist/schedule-zh.json +3 -2
- package/package.json +2 -6
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ export default function Page() {
|
|
|
51
51
|
|
|
52
52
|
The component is SSR-safe: it emits a placeholder during render and hydrates the widget inside `useEffect`. No `window` access at module top-level.
|
|
53
53
|
|
|
54
|
-
## Install —
|
|
54
|
+
## Install — imperative API (NPM)
|
|
55
55
|
|
|
56
56
|
```ts
|
|
57
57
|
import { mount } from 'daily-soup-widget';
|
|
@@ -61,6 +61,9 @@ const handle = mount(host, { lang: 'zh', theme: 'auto' });
|
|
|
61
61
|
// later: handle.destroy();
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
Same React peer requirement as the `<DailySoup>` component — install React + ReactDOM
|
|
65
|
+
(>=18) alongside.
|
|
66
|
+
|
|
64
67
|
---
|
|
65
68
|
|
|
66
69
|
## Configuration
|
|
@@ -130,10 +133,10 @@ scripts/
|
|
|
130
133
|
build-schedule.ts content/*.md → dist/schedule-zh.json
|
|
131
134
|
build-bundle.ts esbuild UMD + ESM + CJS bundles
|
|
132
135
|
src/
|
|
133
|
-
embed.ts UMD entry (CDN, auto-mounts)
|
|
136
|
+
embed.ts UMD entry (CDN, auto-mounts; React bundled in)
|
|
134
137
|
index.ts ESM entry (NPM)
|
|
135
|
-
component.tsx React wrapper
|
|
136
|
-
widget.
|
|
138
|
+
component.tsx React wrapper around mount()
|
|
139
|
+
widget.tsx React widget core (Shadow DOM root, render, fetch)
|
|
137
140
|
theme.ts auto/light/dark resolution + system-theme listener
|
|
138
141
|
i18n.ts UI strings (zh)
|
|
139
142
|
share.ts copy / X / LINE share builders
|
|
@@ -165,6 +168,20 @@ npm run build # full prod build (schedule + bundle + types + Next.j
|
|
|
165
168
|
2. `npm run build:schedule` — extends the schedule, preserving past dates.
|
|
166
169
|
3. Commit + PR. Merge triggers the GH Action to redeploy.
|
|
167
170
|
|
|
171
|
+
### Releasing a new npm version
|
|
172
|
+
|
|
173
|
+
1. Bump `version` in `package.json`, commit, merge to `main`.
|
|
174
|
+
2. Tag and push: `git tag v0.2.3 && git push --tags`.
|
|
175
|
+
3. `.github/workflows/publish.yml` runs `npm test` + `typecheck` + `build:lib` + `npm publish` against the `NPM_TOKEN` repo secret. No local `npm login` ever needed.
|
|
176
|
+
|
|
177
|
+
The `NPM_TOKEN` must be a granular automation token scoped to read & write on `daily-soup-widget`.
|
|
178
|
+
|
|
179
|
+
**Verifying the auth chain without a version bump:** trigger `gh workflow run publish.yml -R mshmwr/daily-soup-widget` against the current `main`. The workflow runs all pre-publish steps and then attempts `npm publish` on the already-published version, which the registry rejects with `E403 You cannot publish over the previously published versions: <version>`. Reaching that exact error confirms the `NPM_TOKEN` secret is present and has publish scope — only the uniqueness check rejected. Use this whenever you rotate the token or change the workflow, instead of minting a throwaway version. Note: `npm whoami` on a granular automation token returns 401 and is not a valid auth-chain test; use `npm publish --dry-run` (locally) or this `workflow_dispatch` trick (in CI) instead.
|
|
180
|
+
|
|
181
|
+
### Auditing source URLs
|
|
182
|
+
|
|
183
|
+
`npm run check:source-urls` HEADs every `sourceUrl` in `content/quotes/**` and flags pages whose body matches `資料已刪除|404 Not Found`. Run before each release; null any broken URLs in frontmatter (the widget renders source as plain text when `sourceUrl` is empty).
|
|
184
|
+
|
|
168
185
|
---
|
|
169
186
|
|
|
170
187
|
## Browser support
|
package/dist/embed.cjs.js
CHANGED
|
@@ -26,6 +26,11 @@ __export(index_exports, {
|
|
|
26
26
|
});
|
|
27
27
|
module.exports = __toCommonJS(index_exports);
|
|
28
28
|
|
|
29
|
+
// src/widget.tsx
|
|
30
|
+
var import_react = require("react");
|
|
31
|
+
var import_client = require("react-dom/client");
|
|
32
|
+
var import_react_dom = require("react-dom");
|
|
33
|
+
|
|
29
34
|
// src/i18n.ts
|
|
30
35
|
var STRINGS = {
|
|
31
36
|
zh: {
|
|
@@ -194,81 +199,9 @@ function todayUtc8(now = /* @__PURE__ */ new Date()) {
|
|
|
194
199
|
return `${yy}-${mm}-${dd}`;
|
|
195
200
|
}
|
|
196
201
|
|
|
197
|
-
// src/widget.
|
|
202
|
+
// src/widget.tsx
|
|
203
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
198
204
|
var DEFAULT_SCHEDULE_BASE = "https://daily-soup-widget.vercel.app";
|
|
199
|
-
function escape(s) {
|
|
200
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
201
|
-
}
|
|
202
|
-
function attachRoot(host) {
|
|
203
|
-
if (typeof host.attachShadow === "function") {
|
|
204
|
-
if (host.shadowRoot) return host.shadowRoot;
|
|
205
|
-
return host.attachShadow({ mode: "open" });
|
|
206
|
-
}
|
|
207
|
-
console.warn("[daily-soup] attachShadow unsupported, falling back to light DOM");
|
|
208
|
-
return host;
|
|
209
|
-
}
|
|
210
|
-
function injectStyles(root) {
|
|
211
|
-
const style = document.createElement("style");
|
|
212
|
-
style.textContent = WIDGET_STYLES;
|
|
213
|
-
root.appendChild(style);
|
|
214
|
-
}
|
|
215
|
-
function buildSkeleton(theme) {
|
|
216
|
-
const card = document.createElement("div");
|
|
217
|
-
card.className = `ds-card ds-${theme} ds-skeleton`;
|
|
218
|
-
card.innerHTML = `
|
|
219
|
-
<div class="ds-quote"> </div>
|
|
220
|
-
<div class="ds-meta"><span class="ds-author"> </span><span class="ds-source"> </span></div>
|
|
221
|
-
`;
|
|
222
|
-
return card;
|
|
223
|
-
}
|
|
224
|
-
function renderQuote(card, quote, lang, theme) {
|
|
225
|
-
const s = t(lang);
|
|
226
|
-
card.className = `ds-card ds-${theme}`;
|
|
227
|
-
const sourceLabel = quote.sourceUrl ? `<a href="${escape(quote.sourceUrl)}" target="_blank" rel="noopener noreferrer">${escape(quote.source)}</a>` : escape(quote.source);
|
|
228
|
-
const flag = quote.attribution === "popular-attribution" ? `<span class="ds-flag">${escape(s.attributedPopular)}</span>` : "";
|
|
229
|
-
card.innerHTML = `
|
|
230
|
-
<p class="ds-quote">${escape(quote.text)}</p>
|
|
231
|
-
<div class="ds-meta">
|
|
232
|
-
<span class="ds-author">\u2014 ${escape(quote.author)}${flag}</span>
|
|
233
|
-
${quote.source ? `<span class="ds-source">${s.source}\uFF1A${sourceLabel}</span>` : ""}
|
|
234
|
-
</div>
|
|
235
|
-
<div class="ds-actions">
|
|
236
|
-
<div class="ds-share">
|
|
237
|
-
<button class="ds-btn" data-action="copy" type="button" aria-label="${escape(s.copy)}">
|
|
238
|
-
<span aria-hidden="true">\u29C9</span><span class="ds-share-label">${escape(s.copy)}</span>
|
|
239
|
-
</button>
|
|
240
|
-
<a class="ds-btn" data-action="x" href="${escape(buildXShareUrl({ text: quote.text, author: quote.author }))}" target="_blank" rel="noopener noreferrer" aria-label="${escape(s.shareX)}">
|
|
241
|
-
<span aria-hidden="true">\u{1D54F}</span><span class="ds-share-label">X</span>
|
|
242
|
-
</a>
|
|
243
|
-
<a class="ds-btn" data-action="line" href="${escape(buildLineShareUrl({ text: quote.text, author: quote.author }))}" target="_blank" rel="noopener noreferrer" aria-label="${escape(s.shareLine)}">
|
|
244
|
-
<span aria-hidden="true">L</span><span class="ds-share-label">LINE</span>
|
|
245
|
-
</a>
|
|
246
|
-
</div>
|
|
247
|
-
<span class="ds-powered"><a href="https://personal-site-mocha-chi.vercel.app" target="_blank" rel="noopener noreferrer">${s.poweredBy}</a></span>
|
|
248
|
-
</div>
|
|
249
|
-
`;
|
|
250
|
-
const copyBtn = card.querySelector('[data-action="copy"]');
|
|
251
|
-
if (copyBtn) {
|
|
252
|
-
copyBtn.addEventListener("click", async () => {
|
|
253
|
-
const ok = await copyToClipboard({ text: quote.text, author: quote.author });
|
|
254
|
-
if (ok) {
|
|
255
|
-
const label = copyBtn.querySelector(".ds-share-label");
|
|
256
|
-
const originalLabel = label?.textContent ?? "";
|
|
257
|
-
if (label) label.textContent = s.copied;
|
|
258
|
-
copyBtn.classList.add("ds-toast");
|
|
259
|
-
setTimeout(() => {
|
|
260
|
-
if (label) label.textContent = originalLabel;
|
|
261
|
-
copyBtn.classList.remove("ds-toast");
|
|
262
|
-
}, 2e3);
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
function renderError(card, lang, theme) {
|
|
268
|
-
card.className = `ds-card ds-${theme}`;
|
|
269
|
-
const s = t(lang);
|
|
270
|
-
card.innerHTML = `<p class="ds-error">${escape(s.loadFailed)}</p>`;
|
|
271
|
-
}
|
|
272
205
|
async function fetchSchedule(scheduleUrl, lang) {
|
|
273
206
|
const base = scheduleUrl.replace(/\/$/, "");
|
|
274
207
|
const url = `${base}/schedule-${lang}.json`;
|
|
@@ -281,8 +214,7 @@ async function fetchSchedule(scheduleUrl, lang) {
|
|
|
281
214
|
return null;
|
|
282
215
|
}
|
|
283
216
|
}
|
|
284
|
-
function pickQuote(schedule) {
|
|
285
|
-
const today = todayUtc8();
|
|
217
|
+
function pickQuote(schedule, today) {
|
|
286
218
|
const id = schedule.entries[today];
|
|
287
219
|
if (id && schedule.quotes[id]) return schedule.quotes[id];
|
|
288
220
|
const fallbackId = Object.keys(schedule.quotes).sort()[0];
|
|
@@ -292,101 +224,231 @@ function pickQuote(schedule) {
|
|
|
292
224
|
}
|
|
293
225
|
return null;
|
|
294
226
|
}
|
|
295
|
-
function
|
|
296
|
-
const
|
|
227
|
+
function buildInlineStyle(themeConfig, maxWidth) {
|
|
228
|
+
const style = {};
|
|
229
|
+
const colors = getThemeColors(themeConfig);
|
|
297
230
|
if (colors) {
|
|
298
|
-
if (colors.bg)
|
|
299
|
-
if (colors.ink)
|
|
300
|
-
if (colors.muted)
|
|
301
|
-
if (colors.border)
|
|
302
|
-
if (colors.accent)
|
|
231
|
+
if (colors.bg) style["--ds-bg"] = colors.bg;
|
|
232
|
+
if (colors.ink) style["--ds-fg"] = colors.ink;
|
|
233
|
+
if (colors.muted) style["--ds-muted"] = colors.muted;
|
|
234
|
+
if (colors.border) style["--ds-border"] = colors.border;
|
|
235
|
+
if (colors.accent) style["--ds-accent"] = colors.accent;
|
|
303
236
|
}
|
|
304
|
-
if (maxWidth)
|
|
237
|
+
if (maxWidth) style["--ds-max-width"] = maxWidth;
|
|
238
|
+
return style;
|
|
239
|
+
}
|
|
240
|
+
function DailySoupWidget({ lang, themeConfig, scheduleUrl, maxWidth }) {
|
|
241
|
+
const [resolvedTheme, setResolvedTheme] = (0, import_react.useState)(() => resolveTheme(themeConfig));
|
|
242
|
+
const [schedule, setSchedule] = (0, import_react.useState)(null);
|
|
243
|
+
const [displayedDate, setDisplayedDate] = (0, import_react.useState)("");
|
|
244
|
+
const [quote, setQuote] = (0, import_react.useState)(null);
|
|
245
|
+
const [loadFailed, setLoadFailed] = (0, import_react.useState)(false);
|
|
246
|
+
const [copyToast, setCopyToast] = (0, import_react.useState)(false);
|
|
247
|
+
(0, import_react.useEffect)(() => {
|
|
248
|
+
if (themeConfig !== "auto") {
|
|
249
|
+
setResolvedTheme(resolveTheme(themeConfig));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
setResolvedTheme(resolveTheme(themeConfig));
|
|
253
|
+
return watchSystemTheme(setResolvedTheme);
|
|
254
|
+
}, [themeConfig]);
|
|
255
|
+
(0, import_react.useEffect)(() => {
|
|
256
|
+
let cancelled = false;
|
|
257
|
+
let retried = false;
|
|
258
|
+
const load = async () => {
|
|
259
|
+
const sch = await fetchSchedule(scheduleUrl, lang);
|
|
260
|
+
if (cancelled) return;
|
|
261
|
+
if (!sch) {
|
|
262
|
+
if (!retried) {
|
|
263
|
+
retried = true;
|
|
264
|
+
setTimeout(load, 2e3);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
setLoadFailed(true);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const today = todayUtc8();
|
|
271
|
+
const q = pickQuote(sch, today);
|
|
272
|
+
if (!q) {
|
|
273
|
+
setLoadFailed(true);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
(0, import_react_dom.flushSync)(() => {
|
|
277
|
+
setSchedule(sch);
|
|
278
|
+
setDisplayedDate(today);
|
|
279
|
+
setQuote(q);
|
|
280
|
+
});
|
|
281
|
+
};
|
|
282
|
+
load();
|
|
283
|
+
return () => {
|
|
284
|
+
cancelled = true;
|
|
285
|
+
};
|
|
286
|
+
}, [lang, scheduleUrl]);
|
|
287
|
+
(0, import_react.useEffect)(() => {
|
|
288
|
+
if (!schedule || typeof document === "undefined") return;
|
|
289
|
+
const onVisibility = () => {
|
|
290
|
+
if (document.visibilityState !== "visible") return;
|
|
291
|
+
const today = todayUtc8();
|
|
292
|
+
if (today === displayedDate) return;
|
|
293
|
+
const q = pickQuote(schedule, today);
|
|
294
|
+
if (!q) return;
|
|
295
|
+
(0, import_react_dom.flushSync)(() => {
|
|
296
|
+
setDisplayedDate(today);
|
|
297
|
+
setQuote(q);
|
|
298
|
+
});
|
|
299
|
+
};
|
|
300
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
301
|
+
return () => document.removeEventListener("visibilitychange", onVisibility);
|
|
302
|
+
}, [schedule, displayedDate]);
|
|
303
|
+
const inlineStyle = buildInlineStyle(themeConfig, maxWidth);
|
|
304
|
+
const s = t(lang);
|
|
305
|
+
const handleCopy = (0, import_react.useCallback)(async () => {
|
|
306
|
+
if (!quote) return;
|
|
307
|
+
const ok = await copyToClipboard({ text: quote.text, author: quote.author });
|
|
308
|
+
if (ok) {
|
|
309
|
+
setCopyToast(true);
|
|
310
|
+
setTimeout(() => setCopyToast(false), 2e3);
|
|
311
|
+
}
|
|
312
|
+
}, [quote]);
|
|
313
|
+
if (loadFailed) {
|
|
314
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
315
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: WIDGET_STYLES }),
|
|
316
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `ds-card ds-${resolvedTheme}`, style: inlineStyle, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "ds-error", children: s.loadFailed }) })
|
|
317
|
+
] });
|
|
318
|
+
}
|
|
319
|
+
if (!quote) {
|
|
320
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
321
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: WIDGET_STYLES }),
|
|
322
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `ds-card ds-${resolvedTheme} ds-skeleton`, style: inlineStyle, children: [
|
|
323
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ds-quote", children: "\xA0".repeat(25) }),
|
|
324
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ds-meta", children: [
|
|
325
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ds-author", children: "\xA0" }),
|
|
326
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ds-source", children: "\xA0" })
|
|
327
|
+
] })
|
|
328
|
+
] })
|
|
329
|
+
] });
|
|
330
|
+
}
|
|
331
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
332
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: WIDGET_STYLES }),
|
|
333
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `ds-card ds-${resolvedTheme}`, style: inlineStyle, children: [
|
|
334
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "ds-quote", children: quote.text }),
|
|
335
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ds-meta", children: [
|
|
336
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "ds-author", children: [
|
|
337
|
+
"\u2014 ",
|
|
338
|
+
quote.author,
|
|
339
|
+
quote.attribution === "popular-attribution" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ds-flag", children: s.attributedPopular })
|
|
340
|
+
] }),
|
|
341
|
+
quote.source && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "ds-source", children: [
|
|
342
|
+
s.source,
|
|
343
|
+
"\uFF1A",
|
|
344
|
+
quote.sourceUrl ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: quote.sourceUrl, target: "_blank", rel: "noopener noreferrer", children: quote.source }) : quote.source
|
|
345
|
+
] })
|
|
346
|
+
] }),
|
|
347
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ds-actions", children: [
|
|
348
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ds-share", children: [
|
|
349
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
350
|
+
"button",
|
|
351
|
+
{
|
|
352
|
+
className: `ds-btn${copyToast ? " ds-toast" : ""}`,
|
|
353
|
+
"data-action": "copy",
|
|
354
|
+
type: "button",
|
|
355
|
+
"aria-label": s.copy,
|
|
356
|
+
onClick: handleCopy,
|
|
357
|
+
children: [
|
|
358
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "\u29C9" }),
|
|
359
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ds-share-label", children: copyToast ? s.copied : s.copy })
|
|
360
|
+
]
|
|
361
|
+
}
|
|
362
|
+
),
|
|
363
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
364
|
+
"a",
|
|
365
|
+
{
|
|
366
|
+
className: "ds-btn",
|
|
367
|
+
"data-action": "x",
|
|
368
|
+
href: buildXShareUrl({ text: quote.text, author: quote.author }),
|
|
369
|
+
target: "_blank",
|
|
370
|
+
rel: "noopener noreferrer",
|
|
371
|
+
"aria-label": s.shareX,
|
|
372
|
+
children: [
|
|
373
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "\u{1D54F}" }),
|
|
374
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ds-share-label", children: "X" })
|
|
375
|
+
]
|
|
376
|
+
}
|
|
377
|
+
),
|
|
378
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
379
|
+
"a",
|
|
380
|
+
{
|
|
381
|
+
className: "ds-btn",
|
|
382
|
+
"data-action": "line",
|
|
383
|
+
href: buildLineShareUrl({ text: quote.text, author: quote.author }),
|
|
384
|
+
target: "_blank",
|
|
385
|
+
rel: "noopener noreferrer",
|
|
386
|
+
"aria-label": s.shareLine,
|
|
387
|
+
children: [
|
|
388
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "L" }),
|
|
389
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ds-share-label", children: "LINE" })
|
|
390
|
+
]
|
|
391
|
+
}
|
|
392
|
+
)
|
|
393
|
+
] }),
|
|
394
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ds-powered", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
395
|
+
"a",
|
|
396
|
+
{
|
|
397
|
+
href: "https://personal-site-mocha-chi.vercel.app",
|
|
398
|
+
target: "_blank",
|
|
399
|
+
rel: "noopener noreferrer",
|
|
400
|
+
children: s.poweredBy
|
|
401
|
+
}
|
|
402
|
+
) })
|
|
403
|
+
] })
|
|
404
|
+
] })
|
|
405
|
+
] });
|
|
406
|
+
}
|
|
407
|
+
function attachRoot(host) {
|
|
408
|
+
if (typeof host.attachShadow === "function") {
|
|
409
|
+
if (host.shadowRoot) return host.shadowRoot;
|
|
410
|
+
return host.attachShadow({ mode: "open" });
|
|
411
|
+
}
|
|
412
|
+
console.warn("[daily-soup] attachShadow unsupported, falling back to light DOM");
|
|
413
|
+
return host;
|
|
305
414
|
}
|
|
306
415
|
function mount(host, options = {}) {
|
|
307
416
|
const lang = options.lang ?? "zh";
|
|
308
417
|
const themeConfig = options.theme ?? "auto";
|
|
309
418
|
const scheduleUrl = options.scheduleUrl === void 0 ? DEFAULT_SCHEDULE_BASE : options.scheduleUrl;
|
|
310
419
|
const maxWidth = options.maxWidth;
|
|
311
|
-
let resolvedTheme = resolveTheme(themeConfig);
|
|
312
420
|
const root = attachRoot(host);
|
|
313
421
|
if (root === host) {
|
|
314
422
|
host.textContent = "";
|
|
315
423
|
} else {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
injectStyles(root);
|
|
319
|
-
const card = buildSkeleton(resolvedTheme);
|
|
320
|
-
applyThemeOverrides(card, themeConfig, maxWidth);
|
|
321
|
-
root.appendChild(card);
|
|
322
|
-
const state = {
|
|
323
|
-
lang,
|
|
324
|
-
themeConfig,
|
|
325
|
-
scheduleUrl,
|
|
326
|
-
host,
|
|
327
|
-
root,
|
|
328
|
-
cardEl: card,
|
|
329
|
-
unwatchTheme: () => {
|
|
330
|
-
},
|
|
331
|
-
unwatchVisibility: () => {
|
|
332
|
-
},
|
|
333
|
-
schedule: null,
|
|
334
|
-
displayedDate: ""
|
|
335
|
-
};
|
|
336
|
-
if (themeConfig === "auto") {
|
|
337
|
-
state.unwatchTheme = watchSystemTheme((t2) => {
|
|
338
|
-
resolvedTheme = t2;
|
|
339
|
-
state.cardEl.classList.remove("ds-light", "ds-dark");
|
|
340
|
-
state.cardEl.classList.add(`ds-${t2}`);
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
let cancelled = false;
|
|
344
|
-
let retried = false;
|
|
345
|
-
const load = async () => {
|
|
346
|
-
const schedule = await fetchSchedule(scheduleUrl, lang);
|
|
347
|
-
if (cancelled) return;
|
|
348
|
-
if (!schedule) {
|
|
349
|
-
if (!retried) {
|
|
350
|
-
retried = true;
|
|
351
|
-
setTimeout(load, 2e3);
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
renderError(state.cardEl, lang, resolvedTheme);
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
state.schedule = schedule;
|
|
358
|
-
const quote = pickQuote(schedule);
|
|
359
|
-
if (!quote) {
|
|
360
|
-
renderError(state.cardEl, lang, resolvedTheme);
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
state.displayedDate = todayUtc8();
|
|
364
|
-
renderQuote(state.cardEl, quote, lang, resolvedTheme);
|
|
365
|
-
};
|
|
366
|
-
load();
|
|
367
|
-
if (typeof document !== "undefined") {
|
|
368
|
-
const onVisibility = () => {
|
|
369
|
-
if (document.visibilityState !== "visible") return;
|
|
370
|
-
if (!state.schedule) return;
|
|
371
|
-
const today = todayUtc8();
|
|
372
|
-
if (today === state.displayedDate) return;
|
|
373
|
-
const quote = pickQuote(state.schedule);
|
|
374
|
-
if (!quote) return;
|
|
375
|
-
state.displayedDate = today;
|
|
376
|
-
renderQuote(state.cardEl, quote, lang, resolvedTheme);
|
|
377
|
-
};
|
|
378
|
-
document.addEventListener("visibilitychange", onVisibility);
|
|
379
|
-
state.unwatchVisibility = () => document.removeEventListener("visibilitychange", onVisibility);
|
|
424
|
+
const shadow = root;
|
|
425
|
+
while (shadow.firstChild) shadow.removeChild(shadow.firstChild);
|
|
380
426
|
}
|
|
427
|
+
const container = document.createElement("div");
|
|
428
|
+
root.appendChild(container);
|
|
429
|
+
const reactRoot = (0, import_client.createRoot)(container);
|
|
430
|
+
(0, import_react_dom.flushSync)(() => {
|
|
431
|
+
reactRoot.render(
|
|
432
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
433
|
+
DailySoupWidget,
|
|
434
|
+
{
|
|
435
|
+
lang,
|
|
436
|
+
themeConfig,
|
|
437
|
+
scheduleUrl,
|
|
438
|
+
maxWidth
|
|
439
|
+
}
|
|
440
|
+
)
|
|
441
|
+
);
|
|
442
|
+
});
|
|
381
443
|
return {
|
|
382
444
|
destroy() {
|
|
383
|
-
|
|
384
|
-
state.unwatchTheme();
|
|
385
|
-
state.unwatchVisibility();
|
|
445
|
+
reactRoot.unmount();
|
|
386
446
|
if (root === host) {
|
|
387
447
|
host.textContent = "";
|
|
388
448
|
} else if (host.shadowRoot) {
|
|
389
|
-
while (host.shadowRoot.firstChild)
|
|
449
|
+
while (host.shadowRoot.firstChild) {
|
|
450
|
+
host.shadowRoot.removeChild(host.shadowRoot.firstChild);
|
|
451
|
+
}
|
|
390
452
|
}
|
|
391
453
|
}
|
|
392
454
|
};
|
|
@@ -406,16 +468,16 @@ function mountAll(selector = "[data-daily-soup], #daily-soup") {
|
|
|
406
468
|
}
|
|
407
469
|
|
|
408
470
|
// src/component.tsx
|
|
409
|
-
var
|
|
410
|
-
var
|
|
471
|
+
var import_react2 = require("react");
|
|
472
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
411
473
|
function DailySoup({ lang = "zh", theme = "auto", scheduleUrl, className, maxWidth }) {
|
|
412
|
-
const hostRef = (0,
|
|
474
|
+
const hostRef = (0, import_react2.useRef)(null);
|
|
413
475
|
const themeKey = typeof theme === "string" ? theme : JSON.stringify(theme);
|
|
414
|
-
(0,
|
|
476
|
+
(0, import_react2.useEffect)(() => {
|
|
415
477
|
if (!hostRef.current) return;
|
|
416
478
|
const handle = mount(hostRef.current, { lang, theme, scheduleUrl, maxWidth });
|
|
417
479
|
return () => handle.destroy();
|
|
418
480
|
}, [lang, themeKey, scheduleUrl, maxWidth]);
|
|
419
|
-
return /* @__PURE__ */ (0,
|
|
481
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ref: hostRef, className, "data-daily-soup-host": "" });
|
|
420
482
|
}
|
|
421
483
|
//# sourceMappingURL=embed.cjs.js.map
|