daily-soup-widget 0.1.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/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/embed.cjs.js +381 -0
- package/dist/embed.cjs.js.map +7 -0
- package/dist/embed.esm.js +358 -0
- package/dist/embed.esm.js.map +7 -0
- package/dist/embed.js +110 -0
- package/dist/embed.js.map +7 -0
- package/dist/schedule-en.json +191 -0
- package/dist/schedule-zh.json +281 -0
- package/dist/types/component.d.ts +2 -0
- package/dist/types/date.d.ts +1 -0
- package/dist/types/embed.d.ts +10 -0
- package/dist/types/i18n.d.ts +14 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/share.d.ts +7 -0
- package/dist/types/styles.d.ts +1 -0
- package/dist/types/theme.d.ts +3 -0
- package/dist/types/types.d.ts +30 -0
- package/dist/types/widget.d.ts +6 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 coco (mshmwr)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Daily Soup Widget
|
|
2
|
+
|
|
3
|
+
Embeddable daily-quote widget. Drop a `<script>` tag on any website, or `npm install` it as a React component. Same deterministic quote everywhere, every day — growth-themed, copyright-safe, zero config.
|
|
4
|
+
|
|
5
|
+
- **Live demo:** https://daily-soup-widget.vercel.app
|
|
6
|
+
- **Source:** https://github.com/mshmwr/daily-soup-widget
|
|
7
|
+
- **License:** MIT
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why this exists
|
|
12
|
+
|
|
13
|
+
A widget shaped like an answer to three questions at once:
|
|
14
|
+
|
|
15
|
+
1. **Technical practice** — Shadow DOM, UMD bundle, container queries, dual NPM/CDN distribution.
|
|
16
|
+
2. **Portfolio piece** — a shipped end-to-end embeddable tool with a hosted live demo.
|
|
17
|
+
3. **Future product** — v1 is intentionally small so v2 (analytics, personalisation, monetisation) has room.
|
|
18
|
+
|
|
19
|
+
The content theme is 人生成長 (personal growth). 30 seed quotes ship in v1; one per day, deterministically scheduled by UTC+8 date. Past dates never change once published.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Install — script tag (any website)
|
|
24
|
+
|
|
25
|
+
```html
|
|
26
|
+
<div id="daily-soup"></div>
|
|
27
|
+
<script src="https://daily-soup-widget.vercel.app/embed.js" async></script>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Optional config via `data-*` attributes:
|
|
31
|
+
|
|
32
|
+
```html
|
|
33
|
+
<div data-daily-soup data-lang="en" data-theme="dark"></div>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Multiple instances on one page are supported — the bundle auto-mounts every `[data-daily-soup]` and `#daily-soup` node it finds.
|
|
37
|
+
|
|
38
|
+
## Install — NPM (React / Next.js)
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install daily-soup-widget
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { DailySoup } from 'daily-soup-widget';
|
|
46
|
+
|
|
47
|
+
export default function Page() {
|
|
48
|
+
return <DailySoup lang="zh" theme="auto" />;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
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
|
+
|
|
54
|
+
## Install — framework-agnostic ESM
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { mount } from 'daily-soup-widget';
|
|
58
|
+
|
|
59
|
+
const host = document.querySelector('#my-mount-point') as HTMLElement;
|
|
60
|
+
const handle = mount(host, { lang: 'zh', theme: 'auto' });
|
|
61
|
+
// later: handle.destroy();
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Configuration
|
|
67
|
+
|
|
68
|
+
| Option | Values | Default | Description |
|
|
69
|
+
| ------------- | ----------------------------------- | -------------------------------- | ----------------------------------------------------------------- |
|
|
70
|
+
| `lang` | `'zh'` / `'en'` | `'zh'` | Content language. One value per widget instance. |
|
|
71
|
+
| `theme` | `'auto'` / `'light'` / `'dark'` | `'auto'` | `auto` follows host's `prefers-color-scheme` and reacts to changes. |
|
|
72
|
+
| `scheduleUrl` | any HTTPS URL or `''` (same-origin) | `https://daily-soup-widget.vercel.app` | Override the CDN that serves `/schedule-<lang>.json`. |
|
|
73
|
+
|
|
74
|
+
Layout is **not** configurable. The widget uses container queries against its own width — drop it in a 200px sidebar or a 700px content column and it adapts. Three breakpoints: <320px (icon-only share row), 320–500px (standard), >500px (larger quote).
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## How "today" is decided
|
|
79
|
+
|
|
80
|
+
`schedule-<lang>.json` ships as a static JSON file mapping calendar date → quote ID:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"launchDate": "2026-05-15",
|
|
85
|
+
"entries": {
|
|
86
|
+
"2026-05-15": "0001",
|
|
87
|
+
"2026-05-16": "0003"
|
|
88
|
+
},
|
|
89
|
+
"quotes": { "0001": { "text": "...", "author": "..." } }
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The client fetches one JSON, computes today's UTC+8 date, looks up the entry, and renders. **No second request. No cold start. No personalisation.** Past dates carry permanent content even if the quote pool reshuffles.
|
|
94
|
+
|
|
95
|
+
A GitHub Actions cron (daily, 02:00 UTC) regenerates the JSON to extend the rolling 90-day window and redeploys. New quotes appended to the pool extend the rotation tail; existing schedule slots stay untouched.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## What's in the box
|
|
100
|
+
|
|
101
|
+
- **30 seed quotes** spanning six growth dimensions: 行動 / 學習 / 堅持 / 心態 / 蛻變 / 使命.
|
|
102
|
+
- **20 Chinese + 10 English.** Of the Chinese: 7 vernacular (白話/半白), 10 classical (文言), 3 poetry (詩詞).
|
|
103
|
+
- **Copyright posture:** all globally public-domain or PD-in-Taiwan. Each quote carries author + source attribution. Six entries marked `popular-attribution` for traditional / popularly-attributed sources.
|
|
104
|
+
|
|
105
|
+
Browse `content/quotes/*.md` to see the seed. Adding a quote = one PR per markdown file.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Repository layout
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
content/quotes/ hand-curated markdown quotes (the source of truth)
|
|
113
|
+
scripts/
|
|
114
|
+
build-schedule.ts content/*.md → dist/schedule-{zh,en}.json
|
|
115
|
+
build-bundle.ts esbuild UMD + ESM + CJS bundles
|
|
116
|
+
src/
|
|
117
|
+
embed.ts UMD entry (CDN, auto-mounts)
|
|
118
|
+
index.ts ESM entry (NPM)
|
|
119
|
+
component.tsx React wrapper
|
|
120
|
+
widget.ts framework-agnostic core (Shadow DOM, render, fetch)
|
|
121
|
+
theme.ts auto/light/dark resolution + system-theme listener
|
|
122
|
+
i18n.ts UI strings (zh, en)
|
|
123
|
+
share.ts copy / X / LINE share builders
|
|
124
|
+
styles.ts widget CSS (injected into Shadow DOM)
|
|
125
|
+
app/ Next.js landing page (live demo + install snippets)
|
|
126
|
+
tests/
|
|
127
|
+
unit/ Vitest — build-schedule, theme, i18n, date
|
|
128
|
+
e2e/ Playwright — landing page smoke
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Local development
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npm install
|
|
137
|
+
npm run build:schedule # content/quotes/*.md → dist/schedule-*.json
|
|
138
|
+
npm run build:bundle # esbuild UMD + ESM + CJS into dist/ + public/
|
|
139
|
+
npm run dev # next dev — landing page at http://localhost:3000
|
|
140
|
+
npm test # vitest
|
|
141
|
+
npm run test:e2e # playwright (requires running dev server)
|
|
142
|
+
npm run typecheck # tsc --noEmit
|
|
143
|
+
npm run build # full prod build (schedule + bundle + types + Next.js)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Adding a quote
|
|
147
|
+
|
|
148
|
+
1. Create `content/quotes/<id>-<slug>.md` with frontmatter (see existing files for the schema).
|
|
149
|
+
2. `npm run build:schedule` — extends the schedule, preserving past dates.
|
|
150
|
+
3. Commit + PR. Merge triggers the GH Action to redeploy.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Browser support
|
|
155
|
+
|
|
156
|
+
- Modern evergreens (Chrome / Safari / Firefox / Edge), 2022+.
|
|
157
|
+
- Container queries: Chrome 105+ / Safari 16+ / Firefox 110+. The widget renders without RWD on older browsers (fixed-size card).
|
|
158
|
+
- Shadow DOM fallback: light-DOM render with a console warning.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT © 2026 coco (mshmwr). See [`LICENSE`](./LICENSE).
|
|
165
|
+
|
|
166
|
+
Quote sources are public-domain or PD-in-Taiwan; see frontmatter on each quote file for attribution.
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DailySoup: () => DailySoup,
|
|
24
|
+
mount: () => mount,
|
|
25
|
+
mountAll: () => mountAll
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// src/i18n.ts
|
|
30
|
+
var STRINGS = {
|
|
31
|
+
zh: {
|
|
32
|
+
copy: "\u8907\u88FD",
|
|
33
|
+
copied: "\u5DF2\u8907\u88FD",
|
|
34
|
+
share: "\u5206\u4EAB",
|
|
35
|
+
source: "\u51FA\u8655",
|
|
36
|
+
poweredBy: "\u7531 coco-c.dev \u63D0\u4F9B",
|
|
37
|
+
attributedPopular: "\u50B3\u7D71\u6B78\u5C6C",
|
|
38
|
+
shareX: "\u5206\u4EAB\u5230 X",
|
|
39
|
+
shareLine: "\u5206\u4EAB\u5230 LINE",
|
|
40
|
+
loadFailed: "\u672C\u65E5\u5C0F\u8A9E\u8F09\u5165\u5931\u6557"
|
|
41
|
+
},
|
|
42
|
+
en: {
|
|
43
|
+
copy: "Copy",
|
|
44
|
+
copied: "Copied!",
|
|
45
|
+
share: "Share",
|
|
46
|
+
source: "Source",
|
|
47
|
+
poweredBy: "powered by coco-c.dev",
|
|
48
|
+
attributedPopular: "popularly attributed",
|
|
49
|
+
shareX: "Share on X",
|
|
50
|
+
shareLine: "Share on LINE",
|
|
51
|
+
loadFailed: "Failed to load daily quote"
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
function t(lang) {
|
|
55
|
+
return STRINGS[lang] ?? STRINGS.zh;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/theme.ts
|
|
59
|
+
function resolveTheme(config) {
|
|
60
|
+
if (config === "light" || config === "dark") return config;
|
|
61
|
+
if (typeof window === "undefined" || !window.matchMedia) return "light";
|
|
62
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
63
|
+
}
|
|
64
|
+
function watchSystemTheme(cb) {
|
|
65
|
+
if (typeof window === "undefined" || !window.matchMedia) return () => {
|
|
66
|
+
};
|
|
67
|
+
const mql = window.matchMedia("(prefers-color-scheme: dark)");
|
|
68
|
+
const handler = (e) => cb(e.matches ? "dark" : "light");
|
|
69
|
+
mql.addEventListener("change", handler);
|
|
70
|
+
return () => mql.removeEventListener("change", handler);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/share.ts
|
|
74
|
+
var SHARE_URL = "https://daily-soup-widget.vercel.app";
|
|
75
|
+
async function copyToClipboard(content) {
|
|
76
|
+
const payload = `${content.text} \u2014 ${content.author}`;
|
|
77
|
+
if (typeof navigator !== "undefined" && navigator.clipboard) {
|
|
78
|
+
try {
|
|
79
|
+
await navigator.clipboard.writeText(payload);
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
function buildXShareUrl(content) {
|
|
88
|
+
const text = encodeURIComponent(`${content.text} \u2014 ${content.author}`);
|
|
89
|
+
const url = encodeURIComponent(SHARE_URL);
|
|
90
|
+
return `https://twitter.com/intent/tweet?text=${text}&url=${url}`;
|
|
91
|
+
}
|
|
92
|
+
function buildLineShareUrl(content) {
|
|
93
|
+
const url = encodeURIComponent(`${SHARE_URL} \u2014 ${content.text}`);
|
|
94
|
+
return `https://social-plugins.line.me/lineit/share?url=${url}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/styles.ts
|
|
98
|
+
var WIDGET_STYLES = `
|
|
99
|
+
:host { all: initial; display: block; font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans TC", sans-serif; }
|
|
100
|
+
* { box-sizing: border-box; }
|
|
101
|
+
.ds-card {
|
|
102
|
+
container-type: inline-size;
|
|
103
|
+
width: 100%;
|
|
104
|
+
max-width: 32rem;
|
|
105
|
+
margin: 0 auto;
|
|
106
|
+
padding: 1.25rem 1.5rem;
|
|
107
|
+
border-radius: 0.75rem;
|
|
108
|
+
border: 1px solid var(--ds-border);
|
|
109
|
+
background: var(--ds-bg);
|
|
110
|
+
color: var(--ds-fg);
|
|
111
|
+
font-size: clamp(0.875rem, 2.5cqi, 1.125rem);
|
|
112
|
+
line-height: 1.7;
|
|
113
|
+
transition: background 0.2s ease, color 0.2s ease;
|
|
114
|
+
}
|
|
115
|
+
.ds-card.ds-light {
|
|
116
|
+
--ds-bg: #fdfcf7;
|
|
117
|
+
--ds-fg: #1f2933;
|
|
118
|
+
--ds-accent: #5b6b9e;
|
|
119
|
+
--ds-muted: #6b7280;
|
|
120
|
+
--ds-border: #e5e7eb;
|
|
121
|
+
}
|
|
122
|
+
.ds-card.ds-dark {
|
|
123
|
+
--ds-bg: #1a1d24;
|
|
124
|
+
--ds-fg: #e5e7eb;
|
|
125
|
+
--ds-accent: #9aa9d4;
|
|
126
|
+
--ds-muted: #9ca3af;
|
|
127
|
+
--ds-border: #2d323d;
|
|
128
|
+
}
|
|
129
|
+
.ds-quote {
|
|
130
|
+
margin: 0 0 0.875rem;
|
|
131
|
+
font-size: 1.1em;
|
|
132
|
+
font-weight: 500;
|
|
133
|
+
letter-spacing: 0.01em;
|
|
134
|
+
white-space: pre-wrap;
|
|
135
|
+
}
|
|
136
|
+
.ds-quote::before { content: '\\201C'; margin-right: 0.15em; color: var(--ds-accent); }
|
|
137
|
+
.ds-quote::after { content: '\\201D'; margin-left: 0.15em; color: var(--ds-accent); }
|
|
138
|
+
.ds-meta { display: flex; flex-direction: column; gap: 0.15rem; margin-bottom: 0.875rem; font-size: 0.875em; color: var(--ds-muted); }
|
|
139
|
+
.ds-author { font-weight: 500; color: var(--ds-fg); }
|
|
140
|
+
.ds-source { font-size: 0.95em; }
|
|
141
|
+
.ds-source a { color: var(--ds-accent); text-decoration: none; }
|
|
142
|
+
.ds-source a:hover { text-decoration: underline; }
|
|
143
|
+
.ds-flag { display: inline-block; margin-left: 0.25rem; padding: 0 0.4rem; font-size: 0.75em; border: 1px solid var(--ds-border); border-radius: 9999px; color: var(--ds-muted); }
|
|
144
|
+
.ds-actions { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-top: 0.875rem; padding-top: 0.875rem; border-top: 1px solid var(--ds-border); }
|
|
145
|
+
.ds-share { display: flex; gap: 0.35rem; }
|
|
146
|
+
.ds-btn {
|
|
147
|
+
appearance: none;
|
|
148
|
+
background: transparent;
|
|
149
|
+
color: var(--ds-fg);
|
|
150
|
+
border: 1px solid var(--ds-border);
|
|
151
|
+
border-radius: 0.5rem;
|
|
152
|
+
padding: 0.3rem 0.7rem;
|
|
153
|
+
font-size: 0.85em;
|
|
154
|
+
font-family: inherit;
|
|
155
|
+
cursor: pointer;
|
|
156
|
+
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
|
157
|
+
text-decoration: none;
|
|
158
|
+
display: inline-flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
gap: 0.25rem;
|
|
161
|
+
}
|
|
162
|
+
.ds-btn:hover { background: var(--ds-accent); color: var(--ds-bg); border-color: var(--ds-accent); }
|
|
163
|
+
.ds-btn:focus-visible { outline: 2px solid var(--ds-accent); outline-offset: 2px; }
|
|
164
|
+
.ds-btn.ds-toast { background: var(--ds-accent); color: var(--ds-bg); border-color: var(--ds-accent); }
|
|
165
|
+
.ds-powered { font-size: 0.75em; color: var(--ds-muted); }
|
|
166
|
+
.ds-powered a { color: var(--ds-muted); text-decoration: none; }
|
|
167
|
+
.ds-powered a:hover { text-decoration: underline; }
|
|
168
|
+
.ds-skeleton .ds-quote { background: var(--ds-border); border-radius: 0.25rem; color: transparent; }
|
|
169
|
+
.ds-skeleton .ds-quote::before, .ds-skeleton .ds-quote::after { content: ''; }
|
|
170
|
+
.ds-error { color: var(--ds-muted); font-size: 0.875em; }
|
|
171
|
+
|
|
172
|
+
@container (max-width: 320px) {
|
|
173
|
+
.ds-card { padding: 1rem 1.1rem; }
|
|
174
|
+
.ds-share-label { display: none; }
|
|
175
|
+
.ds-actions { flex-wrap: wrap; }
|
|
176
|
+
}
|
|
177
|
+
@container (min-width: 500px) {
|
|
178
|
+
.ds-quote { font-size: 1.25em; }
|
|
179
|
+
}
|
|
180
|
+
@media (prefers-reduced-motion: reduce) {
|
|
181
|
+
.ds-card, .ds-btn { transition: none; }
|
|
182
|
+
}
|
|
183
|
+
`;
|
|
184
|
+
|
|
185
|
+
// src/date.ts
|
|
186
|
+
function todayUtc8(now = /* @__PURE__ */ new Date()) {
|
|
187
|
+
const utc8 = new Date(now.getTime() + 8 * 60 * 60 * 1e3);
|
|
188
|
+
const yy = utc8.getUTCFullYear();
|
|
189
|
+
const mm = String(utc8.getUTCMonth() + 1).padStart(2, "0");
|
|
190
|
+
const dd = String(utc8.getUTCDate()).padStart(2, "0");
|
|
191
|
+
return `${yy}-${mm}-${dd}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/widget.ts
|
|
195
|
+
var DEFAULT_SCHEDULE_BASE = "https://daily-soup-widget.vercel.app";
|
|
196
|
+
function escape(s) {
|
|
197
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
198
|
+
}
|
|
199
|
+
function attachRoot(host) {
|
|
200
|
+
if (typeof host.attachShadow === "function") {
|
|
201
|
+
if (host.shadowRoot) return host.shadowRoot;
|
|
202
|
+
return host.attachShadow({ mode: "open" });
|
|
203
|
+
}
|
|
204
|
+
console.warn("[daily-soup] attachShadow unsupported, falling back to light DOM");
|
|
205
|
+
return host;
|
|
206
|
+
}
|
|
207
|
+
function injectStyles(root) {
|
|
208
|
+
const style = document.createElement("style");
|
|
209
|
+
style.textContent = WIDGET_STYLES;
|
|
210
|
+
root.appendChild(style);
|
|
211
|
+
}
|
|
212
|
+
function buildSkeleton(theme) {
|
|
213
|
+
const card = document.createElement("div");
|
|
214
|
+
card.className = `ds-card ds-${theme} ds-skeleton`;
|
|
215
|
+
card.innerHTML = `
|
|
216
|
+
<div class="ds-quote"> </div>
|
|
217
|
+
<div class="ds-meta"><span class="ds-author"> </span><span class="ds-source"> </span></div>
|
|
218
|
+
`;
|
|
219
|
+
return card;
|
|
220
|
+
}
|
|
221
|
+
function renderQuote(card, quote, lang, theme) {
|
|
222
|
+
const s = t(lang);
|
|
223
|
+
card.className = `ds-card ds-${theme}`;
|
|
224
|
+
const sourceLabel = quote.sourceUrl ? `<a href="${escape(quote.sourceUrl)}" target="_blank" rel="noopener noreferrer">${escape(quote.source)}</a>` : escape(quote.source);
|
|
225
|
+
const flag = quote.attribution === "popular-attribution" ? `<span class="ds-flag">${escape(s.attributedPopular)}</span>` : "";
|
|
226
|
+
card.innerHTML = `
|
|
227
|
+
<p class="ds-quote">${escape(quote.text)}</p>
|
|
228
|
+
<div class="ds-meta">
|
|
229
|
+
<span class="ds-author">\u2014 ${escape(quote.author)}${flag}</span>
|
|
230
|
+
${quote.source ? `<span class="ds-source">${s.source}\uFF1A${sourceLabel}</span>` : ""}
|
|
231
|
+
</div>
|
|
232
|
+
<div class="ds-actions">
|
|
233
|
+
<div class="ds-share">
|
|
234
|
+
<button class="ds-btn" data-action="copy" type="button" aria-label="${escape(s.copy)}">
|
|
235
|
+
<span aria-hidden="true">\u29C9</span><span class="ds-share-label">${escape(s.copy)}</span>
|
|
236
|
+
</button>
|
|
237
|
+
<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)}">
|
|
238
|
+
<span aria-hidden="true">\u{1D54F}</span><span class="ds-share-label">X</span>
|
|
239
|
+
</a>
|
|
240
|
+
<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)}">
|
|
241
|
+
<span aria-hidden="true">L</span><span class="ds-share-label">LINE</span>
|
|
242
|
+
</a>
|
|
243
|
+
</div>
|
|
244
|
+
<span class="ds-powered"><a href="https://coco-c.dev" target="_blank" rel="noopener noreferrer">${s.poweredBy}</a></span>
|
|
245
|
+
</div>
|
|
246
|
+
`;
|
|
247
|
+
const copyBtn = card.querySelector('[data-action="copy"]');
|
|
248
|
+
if (copyBtn) {
|
|
249
|
+
copyBtn.addEventListener("click", async () => {
|
|
250
|
+
const ok = await copyToClipboard({ text: quote.text, author: quote.author });
|
|
251
|
+
if (ok) {
|
|
252
|
+
const label = copyBtn.querySelector(".ds-share-label");
|
|
253
|
+
const originalLabel = label?.textContent ?? "";
|
|
254
|
+
if (label) label.textContent = s.copied;
|
|
255
|
+
copyBtn.classList.add("ds-toast");
|
|
256
|
+
setTimeout(() => {
|
|
257
|
+
if (label) label.textContent = originalLabel;
|
|
258
|
+
copyBtn.classList.remove("ds-toast");
|
|
259
|
+
}, 2e3);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function renderError(card, lang, theme) {
|
|
265
|
+
card.className = `ds-card ds-${theme}`;
|
|
266
|
+
const s = t(lang);
|
|
267
|
+
card.innerHTML = `<p class="ds-error">${escape(s.loadFailed)}</p>`;
|
|
268
|
+
}
|
|
269
|
+
async function fetchSchedule(scheduleUrl, lang) {
|
|
270
|
+
const base = scheduleUrl.replace(/\/$/, "");
|
|
271
|
+
const url = `${base}/schedule-${lang}.json`;
|
|
272
|
+
try {
|
|
273
|
+
const init = base === "" ? { credentials: "omit" } : { credentials: "omit", mode: "cors" };
|
|
274
|
+
const res = await fetch(url, init);
|
|
275
|
+
if (!res.ok) return null;
|
|
276
|
+
return await res.json();
|
|
277
|
+
} catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function pickQuote(schedule) {
|
|
282
|
+
const today = todayUtc8();
|
|
283
|
+
const id = schedule.entries[today];
|
|
284
|
+
if (id && schedule.quotes[id]) return schedule.quotes[id];
|
|
285
|
+
const fallbackId = Object.keys(schedule.quotes).sort()[0];
|
|
286
|
+
if (fallbackId && schedule.quotes[fallbackId]) {
|
|
287
|
+
console.warn("[daily-soup] today entry missing or stale, falling back to first quote");
|
|
288
|
+
return schedule.quotes[fallbackId];
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
function mount(host, options = {}) {
|
|
293
|
+
const lang = options.lang ?? "zh";
|
|
294
|
+
const themeConfig = options.theme ?? "auto";
|
|
295
|
+
const scheduleUrl = options.scheduleUrl === void 0 ? DEFAULT_SCHEDULE_BASE : options.scheduleUrl;
|
|
296
|
+
let resolvedTheme = resolveTheme(themeConfig);
|
|
297
|
+
const root = attachRoot(host);
|
|
298
|
+
if (root === host) {
|
|
299
|
+
host.textContent = "";
|
|
300
|
+
} else {
|
|
301
|
+
while (root.firstChild) root.removeChild(root.firstChild);
|
|
302
|
+
}
|
|
303
|
+
injectStyles(root);
|
|
304
|
+
const card = buildSkeleton(resolvedTheme);
|
|
305
|
+
root.appendChild(card);
|
|
306
|
+
const state = {
|
|
307
|
+
lang,
|
|
308
|
+
themeConfig,
|
|
309
|
+
scheduleUrl,
|
|
310
|
+
host,
|
|
311
|
+
root,
|
|
312
|
+
cardEl: card,
|
|
313
|
+
unwatchTheme: () => {
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
if (themeConfig === "auto") {
|
|
317
|
+
state.unwatchTheme = watchSystemTheme((t2) => {
|
|
318
|
+
resolvedTheme = t2;
|
|
319
|
+
state.cardEl.classList.remove("ds-light", "ds-dark");
|
|
320
|
+
state.cardEl.classList.add(`ds-${t2}`);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
let cancelled = false;
|
|
324
|
+
let retried = false;
|
|
325
|
+
const load = async () => {
|
|
326
|
+
const schedule = await fetchSchedule(scheduleUrl, lang);
|
|
327
|
+
if (cancelled) return;
|
|
328
|
+
if (!schedule) {
|
|
329
|
+
if (!retried) {
|
|
330
|
+
retried = true;
|
|
331
|
+
setTimeout(load, 2e3);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
renderError(state.cardEl, lang, resolvedTheme);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const quote = pickQuote(schedule);
|
|
338
|
+
if (!quote) {
|
|
339
|
+
renderError(state.cardEl, lang, resolvedTheme);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
renderQuote(state.cardEl, quote, lang, resolvedTheme);
|
|
343
|
+
};
|
|
344
|
+
load();
|
|
345
|
+
return {
|
|
346
|
+
destroy() {
|
|
347
|
+
cancelled = true;
|
|
348
|
+
state.unwatchTheme();
|
|
349
|
+
if (root === host) {
|
|
350
|
+
host.textContent = "";
|
|
351
|
+
} else if (host.shadowRoot) {
|
|
352
|
+
while (host.shadowRoot.firstChild) host.shadowRoot.removeChild(host.shadowRoot.firstChild);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function mountAll(selector = "[data-daily-soup], #daily-soup") {
|
|
358
|
+
if (typeof document === "undefined") return [];
|
|
359
|
+
const nodes = document.querySelectorAll(selector);
|
|
360
|
+
const handles = [];
|
|
361
|
+
nodes.forEach((node) => {
|
|
362
|
+
const lang = node.dataset.lang ?? "zh";
|
|
363
|
+
const theme = node.dataset.theme ?? "auto";
|
|
364
|
+
const scheduleUrl = node.dataset.scheduleUrl;
|
|
365
|
+
handles.push(mount(node, { lang, theme, scheduleUrl }));
|
|
366
|
+
});
|
|
367
|
+
return handles;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/component.tsx
|
|
371
|
+
var import_react = require("react");
|
|
372
|
+
function DailySoup({ lang = "zh", theme = "auto", scheduleUrl, className }) {
|
|
373
|
+
const hostRef = (0, import_react.useRef)(null);
|
|
374
|
+
(0, import_react.useEffect)(() => {
|
|
375
|
+
if (!hostRef.current) return;
|
|
376
|
+
const handle = mount(hostRef.current, { lang, theme, scheduleUrl });
|
|
377
|
+
return () => handle.destroy();
|
|
378
|
+
}, [lang, theme, scheduleUrl]);
|
|
379
|
+
return /* @__PURE__ */ React.createElement("div", { ref: hostRef, className, "data-daily-soup-host": "" });
|
|
380
|
+
}
|
|
381
|
+
//# sourceMappingURL=embed.cjs.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts", "../src/i18n.ts", "../src/theme.ts", "../src/share.ts", "../src/styles.ts", "../src/date.ts", "../src/widget.ts", "../src/component.tsx"],
|
|
4
|
+
"sourcesContent": ["export { mount, mountAll } from './widget';\nexport { DailySoup } from './component';\nexport type {\n Lang,\n ThemeConfig,\n ResolvedTheme,\n Quote,\n Schedule,\n MountOptions,\n DailySoupProps,\n} from './types';\n", "import type { Lang } from './types';\n\ninterface UiStrings {\n copy: string;\n copied: string;\n share: string;\n source: string;\n poweredBy: string;\n attributedPopular: string;\n shareX: string;\n shareLine: string;\n loadFailed: string;\n}\n\nconst STRINGS: Record<Lang, UiStrings> = {\n zh: {\n copy: '\u8907\u88FD',\n copied: '\u5DF2\u8907\u88FD',\n share: '\u5206\u4EAB',\n source: '\u51FA\u8655',\n poweredBy: '\u7531 coco-c.dev \u63D0\u4F9B',\n attributedPopular: '\u50B3\u7D71\u6B78\u5C6C',\n shareX: '\u5206\u4EAB\u5230 X',\n shareLine: '\u5206\u4EAB\u5230 LINE',\n loadFailed: '\u672C\u65E5\u5C0F\u8A9E\u8F09\u5165\u5931\u6557',\n },\n en: {\n copy: 'Copy',\n copied: 'Copied!',\n share: 'Share',\n source: 'Source',\n poweredBy: 'powered by coco-c.dev',\n attributedPopular: 'popularly attributed',\n shareX: 'Share on X',\n shareLine: 'Share on LINE',\n loadFailed: 'Failed to load daily quote',\n },\n};\n\nexport function t(lang: Lang): UiStrings {\n return STRINGS[lang] ?? STRINGS.zh;\n}\n\nexport type { UiStrings };\n", "import type { ThemeConfig, ResolvedTheme } from './types';\n\nexport function resolveTheme(config: ThemeConfig): ResolvedTheme {\n if (config === 'light' || config === 'dark') return config;\n if (typeof window === 'undefined' || !window.matchMedia) return 'light';\n return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n}\n\nexport function watchSystemTheme(cb: (theme: ResolvedTheme) => void): () => void {\n if (typeof window === 'undefined' || !window.matchMedia) return () => {};\n const mql = window.matchMedia('(prefers-color-scheme: dark)');\n const handler = (e: MediaQueryListEvent) => cb(e.matches ? 'dark' : 'light');\n mql.addEventListener('change', handler);\n return () => mql.removeEventListener('change', handler);\n}\n", "const SHARE_URL = 'https://daily-soup-widget.vercel.app';\n\nexport interface ShareContent {\n text: string;\n author: string;\n}\n\nexport async function copyToClipboard(content: ShareContent): Promise<boolean> {\n const payload = `${content.text} \u2014 ${content.author}`;\n if (typeof navigator !== 'undefined' && navigator.clipboard) {\n try {\n await navigator.clipboard.writeText(payload);\n return true;\n } catch {\n return false;\n }\n }\n return false;\n}\n\nexport function buildXShareUrl(content: ShareContent): string {\n const text = encodeURIComponent(`${content.text} \u2014 ${content.author}`);\n const url = encodeURIComponent(SHARE_URL);\n return `https://twitter.com/intent/tweet?text=${text}&url=${url}`;\n}\n\nexport function buildLineShareUrl(content: ShareContent): string {\n const url = encodeURIComponent(`${SHARE_URL} \u2014 ${content.text}`);\n return `https://social-plugins.line.me/lineit/share?url=${url}`;\n}\n", "export const WIDGET_STYLES = `\n :host { all: initial; display: block; font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, \"Noto Sans TC\", sans-serif; }\n * { box-sizing: border-box; }\n .ds-card {\n container-type: inline-size;\n width: 100%;\n max-width: 32rem;\n margin: 0 auto;\n padding: 1.25rem 1.5rem;\n border-radius: 0.75rem;\n border: 1px solid var(--ds-border);\n background: var(--ds-bg);\n color: var(--ds-fg);\n font-size: clamp(0.875rem, 2.5cqi, 1.125rem);\n line-height: 1.7;\n transition: background 0.2s ease, color 0.2s ease;\n }\n .ds-card.ds-light {\n --ds-bg: #fdfcf7;\n --ds-fg: #1f2933;\n --ds-accent: #5b6b9e;\n --ds-muted: #6b7280;\n --ds-border: #e5e7eb;\n }\n .ds-card.ds-dark {\n --ds-bg: #1a1d24;\n --ds-fg: #e5e7eb;\n --ds-accent: #9aa9d4;\n --ds-muted: #9ca3af;\n --ds-border: #2d323d;\n }\n .ds-quote {\n margin: 0 0 0.875rem;\n font-size: 1.1em;\n font-weight: 500;\n letter-spacing: 0.01em;\n white-space: pre-wrap;\n }\n .ds-quote::before { content: '\\\\201C'; margin-right: 0.15em; color: var(--ds-accent); }\n .ds-quote::after { content: '\\\\201D'; margin-left: 0.15em; color: var(--ds-accent); }\n .ds-meta { display: flex; flex-direction: column; gap: 0.15rem; margin-bottom: 0.875rem; font-size: 0.875em; color: var(--ds-muted); }\n .ds-author { font-weight: 500; color: var(--ds-fg); }\n .ds-source { font-size: 0.95em; }\n .ds-source a { color: var(--ds-accent); text-decoration: none; }\n .ds-source a:hover { text-decoration: underline; }\n .ds-flag { display: inline-block; margin-left: 0.25rem; padding: 0 0.4rem; font-size: 0.75em; border: 1px solid var(--ds-border); border-radius: 9999px; color: var(--ds-muted); }\n .ds-actions { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-top: 0.875rem; padding-top: 0.875rem; border-top: 1px solid var(--ds-border); }\n .ds-share { display: flex; gap: 0.35rem; }\n .ds-btn {\n appearance: none;\n background: transparent;\n color: var(--ds-fg);\n border: 1px solid var(--ds-border);\n border-radius: 0.5rem;\n padding: 0.3rem 0.7rem;\n font-size: 0.85em;\n font-family: inherit;\n cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;\n text-decoration: none;\n display: inline-flex;\n align-items: center;\n gap: 0.25rem;\n }\n .ds-btn:hover { background: var(--ds-accent); color: var(--ds-bg); border-color: var(--ds-accent); }\n .ds-btn:focus-visible { outline: 2px solid var(--ds-accent); outline-offset: 2px; }\n .ds-btn.ds-toast { background: var(--ds-accent); color: var(--ds-bg); border-color: var(--ds-accent); }\n .ds-powered { font-size: 0.75em; color: var(--ds-muted); }\n .ds-powered a { color: var(--ds-muted); text-decoration: none; }\n .ds-powered a:hover { text-decoration: underline; }\n .ds-skeleton .ds-quote { background: var(--ds-border); border-radius: 0.25rem; color: transparent; }\n .ds-skeleton .ds-quote::before, .ds-skeleton .ds-quote::after { content: ''; }\n .ds-error { color: var(--ds-muted); font-size: 0.875em; }\n\n @container (max-width: 320px) {\n .ds-card { padding: 1rem 1.1rem; }\n .ds-share-label { display: none; }\n .ds-actions { flex-wrap: wrap; }\n }\n @container (min-width: 500px) {\n .ds-quote { font-size: 1.25em; }\n }\n @media (prefers-reduced-motion: reduce) {\n .ds-card, .ds-btn { transition: none; }\n }\n`;\n", "export function todayUtc8(now: Date = new Date()): string {\n const utc8 = new Date(now.getTime() + 8 * 60 * 60 * 1000);\n const yy = utc8.getUTCFullYear();\n const mm = String(utc8.getUTCMonth() + 1).padStart(2, '0');\n const dd = String(utc8.getUTCDate()).padStart(2, '0');\n return `${yy}-${mm}-${dd}`;\n}\n", "import type { Lang, MountOptions, Quote, ResolvedTheme, Schedule, ThemeConfig } from './types';\nimport { t } from './i18n';\nimport { resolveTheme, watchSystemTheme } from './theme';\nimport { buildLineShareUrl, buildXShareUrl, copyToClipboard } from './share';\nimport { WIDGET_STYLES } from './styles';\nimport { todayUtc8 } from './date';\n\nconst DEFAULT_SCHEDULE_BASE = 'https://daily-soup-widget.vercel.app';\n\nexport interface MountHandle {\n destroy(): void;\n}\n\ninterface WidgetState {\n lang: Lang;\n themeConfig: ThemeConfig;\n scheduleUrl: string;\n host: HTMLElement;\n root: ShadowRoot | HTMLElement;\n cardEl: HTMLElement;\n unwatchTheme: () => void;\n}\n\nfunction escape(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nfunction attachRoot(host: HTMLElement): ShadowRoot | HTMLElement {\n if (typeof host.attachShadow === 'function') {\n if (host.shadowRoot) return host.shadowRoot;\n return host.attachShadow({ mode: 'open' });\n }\n console.warn('[daily-soup] attachShadow unsupported, falling back to light DOM');\n return host;\n}\n\nfunction injectStyles(root: ShadowRoot | HTMLElement) {\n const style = document.createElement('style');\n style.textContent = WIDGET_STYLES;\n root.appendChild(style);\n}\n\nfunction buildSkeleton(theme: ResolvedTheme): HTMLElement {\n const card = document.createElement('div');\n card.className = `ds-card ds-${theme} ds-skeleton`;\n card.innerHTML = `\n <div class=\"ds-quote\"> </div>\n <div class=\"ds-meta\"><span class=\"ds-author\"> </span><span class=\"ds-source\"> </span></div>\n `;\n return card;\n}\n\nfunction renderQuote(card: HTMLElement, quote: Quote, lang: Lang, theme: ResolvedTheme) {\n const s = t(lang);\n card.className = `ds-card ds-${theme}`;\n const sourceLabel = quote.sourceUrl\n ? `<a href=\"${escape(quote.sourceUrl)}\" target=\"_blank\" rel=\"noopener noreferrer\">${escape(quote.source)}</a>`\n : escape(quote.source);\n const flag = quote.attribution === 'popular-attribution'\n ? `<span class=\"ds-flag\">${escape(s.attributedPopular)}</span>`\n : '';\n card.innerHTML = `\n <p class=\"ds-quote\">${escape(quote.text)}</p>\n <div class=\"ds-meta\">\n <span class=\"ds-author\">\u2014 ${escape(quote.author)}${flag}</span>\n ${quote.source ? `<span class=\"ds-source\">${s.source}\uFF1A${sourceLabel}</span>` : ''}\n </div>\n <div class=\"ds-actions\">\n <div class=\"ds-share\">\n <button class=\"ds-btn\" data-action=\"copy\" type=\"button\" aria-label=\"${escape(s.copy)}\">\n <span aria-hidden=\"true\">\u29C9</span><span class=\"ds-share-label\">${escape(s.copy)}</span>\n </button>\n <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)}\">\n <span aria-hidden=\"true\">\uD835\uDD4F</span><span class=\"ds-share-label\">X</span>\n </a>\n <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)}\">\n <span aria-hidden=\"true\">L</span><span class=\"ds-share-label\">LINE</span>\n </a>\n </div>\n <span class=\"ds-powered\"><a href=\"https://coco-c.dev\" target=\"_blank\" rel=\"noopener noreferrer\">${s.poweredBy}</a></span>\n </div>\n `;\n\n const copyBtn = card.querySelector<HTMLButtonElement>('[data-action=\"copy\"]');\n if (copyBtn) {\n copyBtn.addEventListener('click', async () => {\n const ok = await copyToClipboard({ text: quote.text, author: quote.author });\n if (ok) {\n const label = copyBtn.querySelector('.ds-share-label');\n const originalLabel = label?.textContent ?? '';\n if (label) label.textContent = s.copied;\n copyBtn.classList.add('ds-toast');\n setTimeout(() => {\n if (label) label.textContent = originalLabel;\n copyBtn.classList.remove('ds-toast');\n }, 2000);\n }\n });\n }\n}\n\nfunction renderError(card: HTMLElement, lang: Lang, theme: ResolvedTheme) {\n card.className = `ds-card ds-${theme}`;\n const s = t(lang);\n card.innerHTML = `<p class=\"ds-error\">${escape(s.loadFailed)}</p>`;\n}\n\nasync function fetchSchedule(scheduleUrl: string, lang: Lang): Promise<Schedule | null> {\n const base = scheduleUrl.replace(/\\/$/, '');\n const url = `${base}/schedule-${lang}.json`;\n try {\n const init: RequestInit = base === '' ? { credentials: 'omit' } : { credentials: 'omit', mode: 'cors' };\n const res = await fetch(url, init);\n if (!res.ok) return null;\n return (await res.json()) as Schedule;\n } catch {\n return null;\n }\n}\n\nfunction pickQuote(schedule: Schedule): Quote | null {\n const today = todayUtc8();\n const id = schedule.entries[today];\n if (id && schedule.quotes[id]) return schedule.quotes[id];\n const fallbackId = Object.keys(schedule.quotes).sort()[0];\n if (fallbackId && schedule.quotes[fallbackId]) {\n console.warn('[daily-soup] today entry missing or stale, falling back to first quote');\n return schedule.quotes[fallbackId];\n }\n return null;\n}\n\nexport function mount(host: HTMLElement, options: MountOptions = {}): MountHandle {\n const lang: Lang = options.lang ?? 'zh';\n const themeConfig: ThemeConfig = options.theme ?? 'auto';\n const scheduleUrl = options.scheduleUrl === undefined ? DEFAULT_SCHEDULE_BASE : options.scheduleUrl;\n\n let resolvedTheme = resolveTheme(themeConfig);\n const root = attachRoot(host);\n if (root === host) {\n // light DOM fallback \u2014 clear any prior content\n host.textContent = '';\n } else {\n while ((root as ShadowRoot).firstChild) (root as ShadowRoot).removeChild((root as ShadowRoot).firstChild!);\n }\n injectStyles(root);\n\n const card = buildSkeleton(resolvedTheme);\n root.appendChild(card);\n\n const state: WidgetState = {\n lang,\n themeConfig,\n scheduleUrl,\n host,\n root,\n cardEl: card,\n unwatchTheme: () => {},\n };\n\n if (themeConfig === 'auto') {\n state.unwatchTheme = watchSystemTheme((t) => {\n resolvedTheme = t;\n state.cardEl.classList.remove('ds-light', 'ds-dark');\n state.cardEl.classList.add(`ds-${t}`);\n });\n }\n\n let cancelled = false;\n let retried = false;\n\n const load = async () => {\n const schedule = await fetchSchedule(scheduleUrl, lang);\n if (cancelled) return;\n if (!schedule) {\n if (!retried) {\n retried = true;\n setTimeout(load, 2000);\n return;\n }\n renderError(state.cardEl, lang, resolvedTheme);\n return;\n }\n const quote = pickQuote(schedule);\n if (!quote) {\n renderError(state.cardEl, lang, resolvedTheme);\n return;\n }\n renderQuote(state.cardEl, quote, lang, resolvedTheme);\n };\n load();\n\n return {\n destroy() {\n cancelled = true;\n state.unwatchTheme();\n if (root === host) {\n host.textContent = '';\n } else if (host.shadowRoot) {\n while (host.shadowRoot.firstChild) host.shadowRoot.removeChild(host.shadowRoot.firstChild);\n }\n },\n };\n}\n\nexport function mountAll(selector = '[data-daily-soup], #daily-soup'): MountHandle[] {\n if (typeof document === 'undefined') return [];\n const nodes = document.querySelectorAll<HTMLElement>(selector);\n const handles: MountHandle[] = [];\n nodes.forEach((node) => {\n const lang = (node.dataset.lang as Lang | undefined) ?? 'zh';\n const theme = (node.dataset.theme as ThemeConfig | undefined) ?? 'auto';\n const scheduleUrl = node.dataset.scheduleUrl;\n handles.push(mount(node, { lang, theme, scheduleUrl }));\n });\n return handles;\n}\n", "import { useEffect, useRef } from 'react';\nimport { mount } from './widget';\nimport type { DailySoupProps } from './types';\n\nexport function DailySoup({ lang = 'zh', theme = 'auto', scheduleUrl, className }: DailySoupProps) {\n const hostRef = useRef<HTMLDivElement | null>(null);\n\n useEffect(() => {\n if (!hostRef.current) return;\n const handle = mount(hostRef.current, { lang, theme, scheduleUrl });\n return () => handle.destroy();\n }, [lang, theme, scheduleUrl]);\n\n return <div ref={hostRef} className={className} data-daily-soup-host=\"\" />;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["t"]
|
|
7
|
+
}
|