weifuwu 0.17.19 → 0.17.22
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 +63 -3
- package/dist/client-locale.d.ts +5 -0
- package/dist/client-pref.d.ts +3 -0
- package/dist/client-router.d.ts +1 -1
- package/dist/client-theme.d.ts +7 -0
- package/dist/react.d.ts +3 -1
- package/dist/react.js +143 -70
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -322,8 +322,13 @@ app.use(preferences({ dir: './locales', locale: { default: 'en' }, theme: { defa
|
|
|
322
322
|
| `theme.cookie` | `'theme'` | Cookie name |
|
|
323
323
|
|
|
324
324
|
```tsx
|
|
325
|
-
// Client-side no-refresh switching
|
|
326
|
-
|
|
325
|
+
// Client-side no-refresh switching — import enables it automatically
|
|
326
|
+
import { useLocale, useTheme } from 'weifuwu/react'
|
|
327
|
+
|
|
328
|
+
<Link href="/__lang/zh">中文</Link> // <Link> handles it via interceptor
|
|
329
|
+
<button onClick={() => setLocale('en')}>EN</button> // or programmatic
|
|
330
|
+
const { theme, resolvedTheme, setTheme } = useTheme()
|
|
331
|
+
// resolvedTheme resolves 'system' → 'dark'|'light' based on prefers-color-scheme
|
|
327
332
|
```
|
|
328
333
|
|
|
329
334
|
### queue [B]
|
|
@@ -467,12 +472,15 @@ const navigate = useNavigate() // programmatic: navigate('/contact
|
|
|
467
472
|
const loading = useNavigating() // reactive loading state
|
|
468
473
|
```
|
|
469
474
|
|
|
470
|
-
`navigate()` fetches SSR, extracts `__weifuwu_root`, replaces in-place. `load.ts` runs on server each nav.
|
|
475
|
+
`navigate()` fetches SSR, extracts `__weifuwu_root`, replaces in-place. `load.ts` runs on server each nav.
|
|
476
|
+
|
|
477
|
+
**Preference URLs** (`/__lang/`, `/__theme/`) are intercepted by modular interceptors registered via `addInterceptor()` — no page reload needed. Importing `useLocale` or `useTheme` registers the interceptor automatically.
|
|
471
478
|
|
|
472
479
|
### Client-side hooks
|
|
473
480
|
|
|
474
481
|
```tsx
|
|
475
482
|
import { useWebsocket, useAction, useData, useQueryState, createStore, Head, setCtx } from 'weifuwu/react'
|
|
483
|
+
import { useLocale, useTheme, applyTheme, addInterceptor } from 'weifuwu/react'
|
|
476
484
|
|
|
477
485
|
// WebSocket — auto-reconnecting
|
|
478
486
|
const { send, lastMessage, readyState } = useWebsocket('/ws/chat', { onMessage: (d) => console.log(d), reconnect: { maxRetries: 10, delay: 3000 } })
|
|
@@ -499,6 +507,58 @@ const count = useStore(s => s.count)
|
|
|
499
507
|
setCtx({ locale: 'en', prefs: { locale: 'en' } })
|
|
500
508
|
```
|
|
501
509
|
|
|
510
|
+
### Locale & Theme
|
|
511
|
+
|
|
512
|
+
```tsx
|
|
513
|
+
import { useLocale } from 'weifuwu/react'
|
|
514
|
+
function LangSwitch() {
|
|
515
|
+
const { locale, setLocale, t } = useLocale()
|
|
516
|
+
return <button onClick={() => setLocale('zh-CN')}>{t('switch_lang')}</button>
|
|
517
|
+
}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
| Return | Description |
|
|
521
|
+
|--------|-------------|
|
|
522
|
+
| `locale` | Current locale string (from `ctx.prefs.locale`) |
|
|
523
|
+
| `setLocale(locale)` | Switch locale (calls `navigate('/__lang/' + locale)`) |
|
|
524
|
+
| `t` | Translation function (same as `useCtx().t`) |
|
|
525
|
+
|
|
526
|
+
```tsx
|
|
527
|
+
import { useTheme } from 'weifuwu/react'
|
|
528
|
+
function ThemeToggle() {
|
|
529
|
+
const { theme, resolvedTheme, setTheme } = useTheme()
|
|
530
|
+
return (
|
|
531
|
+
<>
|
|
532
|
+
<span>Current: {resolvedTheme}</span> {/* 'dark' | 'light' — never 'system' */}
|
|
533
|
+
<select value={theme} onChange={e => setTheme(e.target.value)}>
|
|
534
|
+
<option value="light">☀ Light</option>
|
|
535
|
+
<option value="dark">🌙 Dark</option>
|
|
536
|
+
<option value="system">💻 System</option>
|
|
537
|
+
</select>
|
|
538
|
+
</>
|
|
539
|
+
)
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
| Return | Description |
|
|
544
|
+
|--------|-------------|
|
|
545
|
+
| `theme` | Raw preference (`'light'` \| `'dark'` \| `'system'`) |
|
|
546
|
+
| `resolvedTheme` | Resolved value (`'light'` \| `'dark'`) — `'system'` → matchMedia |
|
|
547
|
+
| `setTheme(theme)` | Switch theme (calls `navigate('/__theme/' + theme)`) |
|
|
548
|
+
|
|
549
|
+
**`applyTheme(theme)`** — DOM-only theme application. Sets `data-theme` on `<html>`, registers `matchMedia` listener for `'system'`. Used by the interceptor; exported for custom scenarios.
|
|
550
|
+
|
|
551
|
+
**`addInterceptor(fn)`** — Register a URL interceptor. Interceptors run before SPA navigation; if one returns `true`, `navigate()` skips the fetch-and-swap.
|
|
552
|
+
|
|
553
|
+
```ts
|
|
554
|
+
import { addInterceptor } from 'weifuwu/react'
|
|
555
|
+
addInterceptor(async (url) => {
|
|
556
|
+
if (!url.pathname.startsWith('/__custom/')) return false
|
|
557
|
+
// handle without page reload
|
|
558
|
+
return true
|
|
559
|
+
})
|
|
560
|
+
```
|
|
561
|
+
|
|
502
562
|
### Flash messages
|
|
503
563
|
|
|
504
564
|
```ts
|
package/dist/client-router.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { addInterceptor } from './client-pref.ts';
|
|
1
2
|
export declare function isNavigating(): boolean;
|
|
2
3
|
export declare function onNavigate(fn: (v: boolean) => void): () => void;
|
|
3
4
|
export declare function navigate(href: string): Promise<void>;
|
|
@@ -297,4 +298,3 @@ export declare function Link({ href, children, onClick, prefetch, ...props }: Li
|
|
|
297
298
|
href: string;
|
|
298
299
|
onClick: (e: React.MouseEvent<HTMLAnchorElement>) => void;
|
|
299
300
|
}, HTMLElement>;
|
|
300
|
-
export {};
|
package/dist/react.d.ts
CHANGED
|
@@ -2,8 +2,10 @@ export { useWebsocket } from './use-websocket.ts';
|
|
|
2
2
|
export type { UseWebsocketOptions, UseWebsocketReturn } from './use-websocket.ts';
|
|
3
3
|
export { useAction } from './use-action.ts';
|
|
4
4
|
export type { UseActionOptions, UseActionReturn } from './use-action.ts';
|
|
5
|
-
export { Link, useNavigate, navigate, useNavigating } from './client-router.ts';
|
|
5
|
+
export { Link, useNavigate, navigate, useNavigating, addInterceptor } from './client-router.ts';
|
|
6
6
|
export { TsxContext, useCtx, setCtx } from './tsx-context.ts';
|
|
7
7
|
export { Head } from './head.tsx';
|
|
8
8
|
export { createStore, useData, useQueryState } from './client-state.ts';
|
|
9
9
|
export type { StoreApi } from './client-state.ts';
|
|
10
|
+
export { useLocale } from './client-locale.ts';
|
|
11
|
+
export { useTheme, applyTheme } from './client-theme.ts';
|
package/dist/react.js
CHANGED
|
@@ -141,63 +141,30 @@ function useAction(url, options) {
|
|
|
141
141
|
// client-router.ts
|
|
142
142
|
import { createElement, useCallback as useCallback3, useState as useState3, useEffect as useEffect2 } from "react";
|
|
143
143
|
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
var _ctx = DEFAULT_CTX;
|
|
149
|
-
var _snapshot = { params: _ctx.params, query: _ctx.query, user: _ctx.user, parsed: _ctx.parsed, prefs: _ctx.prefs, env: _ctx.env };
|
|
150
|
-
var _listeners = /* @__PURE__ */ new Set();
|
|
151
|
-
var subscribe = (cb) => {
|
|
152
|
-
_listeners.add(cb);
|
|
153
|
-
return () => {
|
|
154
|
-
_listeners.delete(cb);
|
|
155
|
-
};
|
|
156
|
-
};
|
|
157
|
-
var getSnapshot = () => _snapshot;
|
|
158
|
-
var getServerSnapshot = getSnapshot;
|
|
159
|
-
var _alsGetStore = null;
|
|
160
|
-
function setCtx(value) {
|
|
161
|
-
_ctx = { ..._ctx, ...value };
|
|
162
|
-
_snapshot = { params: _ctx.params, query: _ctx.query, user: _ctx.user, parsed: _ctx.parsed, prefs: _ctx.prefs, env: _ctx.env };
|
|
163
|
-
_listeners.forEach((fn) => fn());
|
|
164
|
-
}
|
|
165
|
-
function _buildT() {
|
|
166
|
-
const messages = typeof window !== "undefined" ? window.__LOCALE_DATA__ : globalThis.__LOCALE_DATA__;
|
|
167
|
-
if (!messages) return fallbackT;
|
|
168
|
-
return (key, params, fallback) => {
|
|
169
|
-
const msg = key.split(".").reduce((o, k) => o?.[k], messages);
|
|
170
|
-
if (msg === void 0 || msg === null) return fallback ?? key;
|
|
171
|
-
if (!params) return String(msg);
|
|
172
|
-
let result = String(msg);
|
|
173
|
-
for (const [k, v] of Object.entries(params)) result = result.replace(`{${k}}`, v);
|
|
174
|
-
return result;
|
|
175
|
-
};
|
|
144
|
+
// client-pref.ts
|
|
145
|
+
var interceptors = [];
|
|
146
|
+
function addInterceptor(fn) {
|
|
147
|
+
interceptors.push(fn);
|
|
176
148
|
}
|
|
177
|
-
function
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return
|
|
182
|
-
}
|
|
183
|
-
function useCtx() {
|
|
184
|
-
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
185
|
-
return _readCtx();
|
|
149
|
+
async function runInterceptors(url) {
|
|
150
|
+
for (const fn of interceptors) {
|
|
151
|
+
if (await fn(url)) return true;
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
186
154
|
}
|
|
187
|
-
var TsxContext = createContext(DEFAULT_CTX);
|
|
188
155
|
|
|
189
156
|
// client-router.ts
|
|
190
157
|
var _navigating = false;
|
|
191
|
-
var
|
|
158
|
+
var _listeners = [];
|
|
192
159
|
function onNavigate(fn) {
|
|
193
|
-
|
|
160
|
+
_listeners.push(fn);
|
|
194
161
|
return () => {
|
|
195
|
-
|
|
162
|
+
_listeners = _listeners.filter((l) => l !== fn);
|
|
196
163
|
};
|
|
197
164
|
}
|
|
198
165
|
function setNavigating(v) {
|
|
199
166
|
_navigating = v;
|
|
200
|
-
for (const fn of
|
|
167
|
+
for (const fn of _listeners) fn(v);
|
|
201
168
|
}
|
|
202
169
|
async function navigate(href) {
|
|
203
170
|
if (typeof document === "undefined") return;
|
|
@@ -206,30 +173,7 @@ async function navigate(href) {
|
|
|
206
173
|
location.href = href;
|
|
207
174
|
return;
|
|
208
175
|
}
|
|
209
|
-
|
|
210
|
-
const themeMatch = url.pathname.match(/^\/__theme\/(\w+)$/);
|
|
211
|
-
if (langMatch || themeMatch) {
|
|
212
|
-
try {
|
|
213
|
-
const res = await fetch(url.pathname, {
|
|
214
|
-
headers: { accept: "application/json" }
|
|
215
|
-
});
|
|
216
|
-
const data = await res.json();
|
|
217
|
-
const ctx = { ...window.__WEIFUWU_CTX || {}, params: {}, query: {} };
|
|
218
|
-
if (data.locale) {
|
|
219
|
-
ctx.prefs = { ...ctx.prefs, locale: data.locale };
|
|
220
|
-
if (data.messages) window.__LOCALE_DATA__ = data.messages;
|
|
221
|
-
}
|
|
222
|
-
if (data.theme) {
|
|
223
|
-
ctx.prefs = { ...ctx.prefs, theme: data.theme };
|
|
224
|
-
}
|
|
225
|
-
;
|
|
226
|
-
window.__WEIFUWU_CTX = ctx;
|
|
227
|
-
setCtx(ctx);
|
|
228
|
-
} catch {
|
|
229
|
-
location.href = href;
|
|
230
|
-
}
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
176
|
+
if (await runInterceptors(url)) return;
|
|
233
177
|
const scrollPos = [window.scrollX, window.scrollY];
|
|
234
178
|
setNavigating(true);
|
|
235
179
|
try {
|
|
@@ -386,6 +330,51 @@ async function prefetchPage(href) {
|
|
|
386
330
|
}
|
|
387
331
|
}
|
|
388
332
|
|
|
333
|
+
// tsx-context.ts
|
|
334
|
+
import { useSyncExternalStore, createContext } from "react";
|
|
335
|
+
var fallbackT = (key, _params, fallback) => fallback ?? key;
|
|
336
|
+
var DEFAULT_CTX = { params: {}, query: {}, parsed: {}, prefs: {}, env: {}, t: fallbackT, user: {} };
|
|
337
|
+
var _ctx = DEFAULT_CTX;
|
|
338
|
+
var _snapshot = { params: _ctx.params, query: _ctx.query, user: _ctx.user, parsed: _ctx.parsed, prefs: _ctx.prefs, env: _ctx.env };
|
|
339
|
+
var _listeners2 = /* @__PURE__ */ new Set();
|
|
340
|
+
var subscribe = (cb) => {
|
|
341
|
+
_listeners2.add(cb);
|
|
342
|
+
return () => {
|
|
343
|
+
_listeners2.delete(cb);
|
|
344
|
+
};
|
|
345
|
+
};
|
|
346
|
+
var getSnapshot = () => _snapshot;
|
|
347
|
+
var getServerSnapshot = getSnapshot;
|
|
348
|
+
var _alsGetStore = null;
|
|
349
|
+
function setCtx(value) {
|
|
350
|
+
_ctx = { ..._ctx, ...value };
|
|
351
|
+
_snapshot = { params: _ctx.params, query: _ctx.query, user: _ctx.user, parsed: _ctx.parsed, prefs: _ctx.prefs, env: _ctx.env };
|
|
352
|
+
_listeners2.forEach((fn) => fn());
|
|
353
|
+
}
|
|
354
|
+
function _buildT() {
|
|
355
|
+
const messages = typeof window !== "undefined" ? window.__LOCALE_DATA__ : globalThis.__LOCALE_DATA__;
|
|
356
|
+
if (!messages) return fallbackT;
|
|
357
|
+
return (key, params, fallback) => {
|
|
358
|
+
const msg = key.split(".").reduce((o, k) => o?.[k], messages);
|
|
359
|
+
if (msg === void 0 || msg === null) return fallback ?? key;
|
|
360
|
+
if (!params) return String(msg);
|
|
361
|
+
let result = String(msg);
|
|
362
|
+
for (const [k, v] of Object.entries(params)) result = result.replace(`{${k}}`, v);
|
|
363
|
+
return result;
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function _readCtx() {
|
|
367
|
+
const alsStore = _alsGetStore?.();
|
|
368
|
+
const base = alsStore ?? _ctx;
|
|
369
|
+
const data = typeof window !== "undefined" ? window.__WEIFUWU_CTX : null;
|
|
370
|
+
return { ...base, ...data, t: _buildT() };
|
|
371
|
+
}
|
|
372
|
+
function useCtx() {
|
|
373
|
+
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
374
|
+
return _readCtx();
|
|
375
|
+
}
|
|
376
|
+
var TsxContext = createContext(DEFAULT_CTX);
|
|
377
|
+
|
|
389
378
|
// head.tsx
|
|
390
379
|
import { createElement as createElement2 } from "react";
|
|
391
380
|
function Head({ children }) {
|
|
@@ -524,18 +513,102 @@ function useQueryState(key, defaultValue = "") {
|
|
|
524
513
|
}, [key, defaultValue]);
|
|
525
514
|
return [value, setValue];
|
|
526
515
|
}
|
|
516
|
+
|
|
517
|
+
// client-locale.ts
|
|
518
|
+
addInterceptor(async (url) => {
|
|
519
|
+
const m = url.pathname.match(/^\/__lang\/(\w+)$/);
|
|
520
|
+
if (!m) return false;
|
|
521
|
+
try {
|
|
522
|
+
const res = await fetch(url.pathname, {
|
|
523
|
+
headers: { accept: "application/json" }
|
|
524
|
+
});
|
|
525
|
+
const data = await res.json();
|
|
526
|
+
const ctx = { ...window.__WEIFUWU_CTX || {}, params: {}, query: {} };
|
|
527
|
+
ctx.prefs = { ...ctx.prefs, locale: data.locale };
|
|
528
|
+
if (data.messages) window.__LOCALE_DATA__ = data.messages;
|
|
529
|
+
window.__WEIFUWU_CTX = ctx;
|
|
530
|
+
setCtx(ctx);
|
|
531
|
+
} catch {
|
|
532
|
+
location.href = url.href;
|
|
533
|
+
}
|
|
534
|
+
return true;
|
|
535
|
+
});
|
|
536
|
+
function useLocale() {
|
|
537
|
+
const ctx = useCtx();
|
|
538
|
+
return {
|
|
539
|
+
locale: ctx.prefs.locale,
|
|
540
|
+
setLocale: (locale) => navigate("/__lang/" + locale),
|
|
541
|
+
t: ctx.t
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// client-theme.ts
|
|
546
|
+
function resolveTheme(theme) {
|
|
547
|
+
if (theme === "system" && typeof window !== "undefined") {
|
|
548
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
549
|
+
}
|
|
550
|
+
return theme;
|
|
551
|
+
}
|
|
552
|
+
var _mqListener = null;
|
|
553
|
+
function applyTheme(theme) {
|
|
554
|
+
if (typeof document === "undefined") return;
|
|
555
|
+
const resolved = resolveTheme(theme);
|
|
556
|
+
document.documentElement.dataset.theme = resolved;
|
|
557
|
+
if (theme === "system") {
|
|
558
|
+
if (!_mqListener) {
|
|
559
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
560
|
+
mq.addEventListener("change", (e) => {
|
|
561
|
+
if (window.__WEIFUWU_CTX?.prefs?.theme === "system") {
|
|
562
|
+
document.documentElement.dataset.theme = e.matches ? "dark" : "light";
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
_mqListener = mq;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
addInterceptor(async (url) => {
|
|
570
|
+
const m = url.pathname.match(/^\/__theme\/(\w+)$/);
|
|
571
|
+
if (!m) return false;
|
|
572
|
+
try {
|
|
573
|
+
const res = await fetch(url.pathname, {
|
|
574
|
+
headers: { accept: "application/json" }
|
|
575
|
+
});
|
|
576
|
+
const data = await res.json();
|
|
577
|
+
const ctx = { ...window.__WEIFUWU_CTX || {}, params: {}, query: {} };
|
|
578
|
+
ctx.prefs = { ...ctx.prefs, theme: data.theme };
|
|
579
|
+
window.__WEIFUWU_CTX = ctx;
|
|
580
|
+
applyTheme(data.theme);
|
|
581
|
+
setCtx(ctx);
|
|
582
|
+
} catch {
|
|
583
|
+
location.href = url.href;
|
|
584
|
+
}
|
|
585
|
+
return true;
|
|
586
|
+
});
|
|
587
|
+
function useTheme() {
|
|
588
|
+
const ctx = useCtx();
|
|
589
|
+
const theme = ctx.prefs.theme ?? "system";
|
|
590
|
+
return {
|
|
591
|
+
theme,
|
|
592
|
+
resolvedTheme: resolveTheme(theme),
|
|
593
|
+
setTheme: (t) => navigate("/__theme/" + t)
|
|
594
|
+
};
|
|
595
|
+
}
|
|
527
596
|
export {
|
|
528
597
|
Head,
|
|
529
598
|
Link,
|
|
530
599
|
TsxContext,
|
|
600
|
+
addInterceptor,
|
|
601
|
+
applyTheme,
|
|
531
602
|
createStore,
|
|
532
603
|
navigate,
|
|
533
604
|
setCtx,
|
|
534
605
|
useAction,
|
|
535
606
|
useCtx,
|
|
536
607
|
useData,
|
|
608
|
+
useLocale,
|
|
537
609
|
useNavigate,
|
|
538
610
|
useNavigating,
|
|
539
611
|
useQueryState,
|
|
612
|
+
useTheme,
|
|
540
613
|
useWebsocket
|
|
541
614
|
};
|