@windrun-huaiin/third-ui 14.4.1 → 14.4.3
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/dist/clerk/fingerprint/fingerprint-debug.d.ts +3 -0
- package/dist/clerk/fingerprint/fingerprint-debug.js +24 -2
- package/dist/clerk/fingerprint/fingerprint-debug.mjs +23 -4
- package/dist/clerk/fingerprint/fingerprint-provider.js +8 -16
- package/dist/clerk/fingerprint/fingerprint-provider.mjs +9 -17
- package/dist/clerk/fingerprint/fingerprint-shared.d.ts +3 -0
- package/dist/clerk/fingerprint/fingerprint-shared.js +12 -1
- package/dist/clerk/fingerprint/fingerprint-shared.mjs +11 -2
- package/dist/clerk/fingerprint/index.js +2 -0
- package/dist/clerk/fingerprint/index.mjs +1 -1
- package/dist/clerk/fingerprint/server.js +2 -0
- package/dist/clerk/fingerprint/server.mjs +1 -1
- package/dist/clerk/fingerprint/use-fingerprint.js +7 -2
- package/dist/clerk/fingerprint/use-fingerprint.mjs +9 -4
- package/dist/fuma/mdx/toc-clerk-portable.js +35 -5
- package/dist/fuma/mdx/toc-clerk-portable.mjs +35 -5
- package/package.json +1 -1
- package/src/clerk/fingerprint/fingerprint-debug.ts +27 -2
- package/src/clerk/fingerprint/fingerprint-provider.tsx +10 -19
- package/src/clerk/fingerprint/fingerprint-shared.ts +12 -1
- package/src/clerk/fingerprint/use-fingerprint.ts +12 -4
- package/src/fuma/mdx/toc-clerk-portable.tsx +41 -3
- package/src/styles/fuma.css +15 -2
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
export declare function getDebugFingerprintOverride(): string | null;
|
|
2
2
|
export declare function setDebugFingerprintOverride(fingerprintId: string): void;
|
|
3
|
+
export declare function buildDebugFingerprintId(): string;
|
|
4
|
+
export declare function getOrCreateDebugFingerprintOverride(): string;
|
|
5
|
+
export declare function regenerateDebugFingerprintOverride(): string;
|
|
@@ -27,17 +27,39 @@ function setLocalStorageValue(key, value) {
|
|
|
27
27
|
}
|
|
28
28
|
function getDebugFingerprintOverride() {
|
|
29
29
|
const value = getLocalStorageValue(fingerprintShared.FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY);
|
|
30
|
-
if (!value || !fingerprintShared.isValidFingerprintId(value)) {
|
|
30
|
+
if (!value || !fingerprintShared.isValidFingerprintId(value) || !fingerprintShared.isDebugFingerprintId(value)) {
|
|
31
31
|
return null;
|
|
32
32
|
}
|
|
33
33
|
return value;
|
|
34
34
|
}
|
|
35
35
|
function setDebugFingerprintOverride(fingerprintId) {
|
|
36
|
-
if (!fingerprintShared.isValidFingerprintId(fingerprintId)) {
|
|
36
|
+
if (!fingerprintShared.isValidFingerprintId(fingerprintId) || !fingerprintShared.isDebugFingerprintId(fingerprintId)) {
|
|
37
37
|
throw new Error('Invalid fingerprint ID');
|
|
38
38
|
}
|
|
39
39
|
setLocalStorageValue(fingerprintShared.FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY, fingerprintId);
|
|
40
40
|
}
|
|
41
|
+
function buildDebugFingerprintId() {
|
|
42
|
+
const timestamp = new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14);
|
|
43
|
+
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
44
|
+
return `${fingerprintShared.FINGERPRINT_DEBUG_PREFIX}${timestamp}_${randomSuffix}`;
|
|
45
|
+
}
|
|
46
|
+
function getOrCreateDebugFingerprintOverride() {
|
|
47
|
+
const existing = getDebugFingerprintOverride();
|
|
48
|
+
if (existing) {
|
|
49
|
+
return existing;
|
|
50
|
+
}
|
|
51
|
+
const nextFingerprintId = buildDebugFingerprintId();
|
|
52
|
+
setDebugFingerprintOverride(nextFingerprintId);
|
|
53
|
+
return nextFingerprintId;
|
|
54
|
+
}
|
|
55
|
+
function regenerateDebugFingerprintOverride() {
|
|
56
|
+
const nextFingerprintId = buildDebugFingerprintId();
|
|
57
|
+
setDebugFingerprintOverride(nextFingerprintId);
|
|
58
|
+
return nextFingerprintId;
|
|
59
|
+
}
|
|
41
60
|
|
|
61
|
+
exports.buildDebugFingerprintId = buildDebugFingerprintId;
|
|
42
62
|
exports.getDebugFingerprintOverride = getDebugFingerprintOverride;
|
|
63
|
+
exports.getOrCreateDebugFingerprintOverride = getOrCreateDebugFingerprintOverride;
|
|
64
|
+
exports.regenerateDebugFingerprintOverride = regenerateDebugFingerprintOverride;
|
|
43
65
|
exports.setDebugFingerprintOverride = setDebugFingerprintOverride;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { isValidFingerprintId, FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY } from './fingerprint-shared.mjs';
|
|
2
|
+
import { isValidFingerprintId, isDebugFingerprintId, FINGERPRINT_DEBUG_PREFIX, FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY } from './fingerprint-shared.mjs';
|
|
3
3
|
|
|
4
4
|
function getLocalStorageValue(key) {
|
|
5
5
|
if (typeof window === 'undefined') {
|
|
@@ -25,16 +25,35 @@ function setLocalStorageValue(key, value) {
|
|
|
25
25
|
}
|
|
26
26
|
function getDebugFingerprintOverride() {
|
|
27
27
|
const value = getLocalStorageValue(FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY);
|
|
28
|
-
if (!value || !isValidFingerprintId(value)) {
|
|
28
|
+
if (!value || !isValidFingerprintId(value) || !isDebugFingerprintId(value)) {
|
|
29
29
|
return null;
|
|
30
30
|
}
|
|
31
31
|
return value;
|
|
32
32
|
}
|
|
33
33
|
function setDebugFingerprintOverride(fingerprintId) {
|
|
34
|
-
if (!isValidFingerprintId(fingerprintId)) {
|
|
34
|
+
if (!isValidFingerprintId(fingerprintId) || !isDebugFingerprintId(fingerprintId)) {
|
|
35
35
|
throw new Error('Invalid fingerprint ID');
|
|
36
36
|
}
|
|
37
37
|
setLocalStorageValue(FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY, fingerprintId);
|
|
38
38
|
}
|
|
39
|
+
function buildDebugFingerprintId() {
|
|
40
|
+
const timestamp = new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14);
|
|
41
|
+
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
42
|
+
return `${FINGERPRINT_DEBUG_PREFIX}${timestamp}_${randomSuffix}`;
|
|
43
|
+
}
|
|
44
|
+
function getOrCreateDebugFingerprintOverride() {
|
|
45
|
+
const existing = getDebugFingerprintOverride();
|
|
46
|
+
if (existing) {
|
|
47
|
+
return existing;
|
|
48
|
+
}
|
|
49
|
+
const nextFingerprintId = buildDebugFingerprintId();
|
|
50
|
+
setDebugFingerprintOverride(nextFingerprintId);
|
|
51
|
+
return nextFingerprintId;
|
|
52
|
+
}
|
|
53
|
+
function regenerateDebugFingerprintOverride() {
|
|
54
|
+
const nextFingerprintId = buildDebugFingerprintId();
|
|
55
|
+
setDebugFingerprintOverride(nextFingerprintId);
|
|
56
|
+
return nextFingerprintId;
|
|
57
|
+
}
|
|
39
58
|
|
|
40
|
-
export { getDebugFingerprintOverride, setDebugFingerprintOverride };
|
|
59
|
+
export { buildDebugFingerprintId, getDebugFingerprintOverride, getOrCreateDebugFingerprintOverride, regenerateDebugFingerprintOverride, setDebugFingerprintOverride };
|
|
@@ -81,15 +81,11 @@ function FingerprintStatus() {
|
|
|
81
81
|
}
|
|
82
82
|
}, [xUser]);
|
|
83
83
|
React.useEffect(() => {
|
|
84
|
-
|
|
85
|
-
if (debugFingerprintOverride) {
|
|
86
|
-
setActiveDebugFingerprintId(debugFingerprintOverride);
|
|
84
|
+
if (panelMode !== 'test') {
|
|
87
85
|
return;
|
|
88
86
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
setActiveDebugFingerprintId(nextFingerprintId);
|
|
92
|
-
}, []);
|
|
87
|
+
setActiveDebugFingerprintId((current) => current !== null && current !== void 0 ? current : fingerprintDebug.getOrCreateDebugFingerprintOverride());
|
|
88
|
+
}, [panelMode]);
|
|
93
89
|
const creditBuckets = React.useMemo(() => {
|
|
94
90
|
if (!xCredit)
|
|
95
91
|
return [];
|
|
@@ -147,11 +143,12 @@ function FingerprintStatus() {
|
|
|
147
143
|
const subStatus = subscriptionStatus.status;
|
|
148
144
|
const themedGhostButtonClass = utils.cn('border-slate-200 bg-white/90 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950/80 dark:hover:bg-slate-900', 'hover:border-current', lib.themeIconColor);
|
|
149
145
|
const runContextParallelInitTest = () => tslib_es6.__awaiter(this, void 0, void 0, function* () {
|
|
150
|
-
const debugFingerprintId = activeDebugFingerprintId;
|
|
146
|
+
const debugFingerprintId = activeDebugFingerprintId !== null && activeDebugFingerprintId !== void 0 ? activeDebugFingerprintId : fingerprintDebug.getOrCreateDebugFingerprintOverride();
|
|
151
147
|
if (!debugFingerprintId) {
|
|
152
148
|
setTestResult('Test fingerprint override is not ready yet.');
|
|
153
149
|
return;
|
|
154
150
|
}
|
|
151
|
+
setActiveDebugFingerprintId(debugFingerprintId);
|
|
155
152
|
setIsRunningTest(true);
|
|
156
153
|
setTestResult(`Running Frontend Prevention Test with fingerprint: ${debugFingerprintId}`);
|
|
157
154
|
try {
|
|
@@ -192,11 +189,12 @@ function FingerprintStatus() {
|
|
|
192
189
|
}
|
|
193
190
|
});
|
|
194
191
|
const runRawParallelPostTest = () => tslib_es6.__awaiter(this, void 0, void 0, function* () {
|
|
195
|
-
const normalizedFingerprintId = activeDebugFingerprintId;
|
|
192
|
+
const normalizedFingerprintId = activeDebugFingerprintId !== null && activeDebugFingerprintId !== void 0 ? activeDebugFingerprintId : fingerprintDebug.getOrCreateDebugFingerprintOverride();
|
|
196
193
|
if (!normalizedFingerprintId) {
|
|
197
194
|
setTestResult('Test fingerprint override is not ready yet.');
|
|
198
195
|
return;
|
|
199
196
|
}
|
|
197
|
+
setActiveDebugFingerprintId(normalizedFingerprintId);
|
|
200
198
|
setIsRunningTest(true);
|
|
201
199
|
setTestResult(`Running Backend Idempotency Test with fingerprint: ${normalizedFingerprintId}`);
|
|
202
200
|
try {
|
|
@@ -245,8 +243,7 @@ function FingerprintStatus() {
|
|
|
245
243
|
}
|
|
246
244
|
});
|
|
247
245
|
const regenerateTestFingerprint = () => {
|
|
248
|
-
const nextFingerprintId =
|
|
249
|
-
fingerprintDebug.setDebugFingerprintOverride(nextFingerprintId);
|
|
246
|
+
const nextFingerprintId = fingerprintDebug.regenerateDebugFingerprintOverride();
|
|
250
247
|
setActiveDebugFingerprintId(nextFingerprintId);
|
|
251
248
|
setTestResult(`Generated test fingerprint override: ${nextFingerprintId}`);
|
|
252
249
|
};
|
|
@@ -338,11 +335,6 @@ function StatusTag({ value }) {
|
|
|
338
335
|
const badgeClass = colorMap[normalized] || defaultColor;
|
|
339
336
|
return (jsxRuntime.jsx("span", { className: utils.cn('inline-block rounded-full px-2 py-0.5 text-xs capitalize font-medium', badgeClass), children: value }));
|
|
340
337
|
}
|
|
341
|
-
function buildDebugFingerprintId() {
|
|
342
|
-
const timestamp = new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14);
|
|
343
|
-
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
344
|
-
return `fp_test_dbg_${timestamp}_${randomSuffix}`;
|
|
345
|
-
}
|
|
346
338
|
function formatErrorMessage(error) {
|
|
347
339
|
if (error instanceof Error) {
|
|
348
340
|
return error.message;
|
|
@@ -8,7 +8,7 @@ import { createContext, useContext, useState, useRef, useEffect, useMemo } from
|
|
|
8
8
|
import { useFingerprint } from './use-fingerprint.mjs';
|
|
9
9
|
import { CopyableText } from '@windrun-huaiin/base-ui/ui';
|
|
10
10
|
import { createFingerprintHeaders } from './fingerprint-client.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import { getOrCreateDebugFingerprintOverride, regenerateDebugFingerprintOverride } from './fingerprint-debug.mjs';
|
|
12
12
|
import { FINGERPRINT_SOURCE_REFER } from './fingerprint-shared.mjs';
|
|
13
13
|
|
|
14
14
|
const FingerprintContext = createContext(undefined);
|
|
@@ -79,15 +79,11 @@ function FingerprintStatus() {
|
|
|
79
79
|
}
|
|
80
80
|
}, [xUser]);
|
|
81
81
|
useEffect(() => {
|
|
82
|
-
|
|
83
|
-
if (debugFingerprintOverride) {
|
|
84
|
-
setActiveDebugFingerprintId(debugFingerprintOverride);
|
|
82
|
+
if (panelMode !== 'test') {
|
|
85
83
|
return;
|
|
86
84
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
setActiveDebugFingerprintId(nextFingerprintId);
|
|
90
|
-
}, []);
|
|
85
|
+
setActiveDebugFingerprintId((current) => current !== null && current !== void 0 ? current : getOrCreateDebugFingerprintOverride());
|
|
86
|
+
}, [panelMode]);
|
|
91
87
|
const creditBuckets = useMemo(() => {
|
|
92
88
|
if (!xCredit)
|
|
93
89
|
return [];
|
|
@@ -145,11 +141,12 @@ function FingerprintStatus() {
|
|
|
145
141
|
const subStatus = subscriptionStatus.status;
|
|
146
142
|
const themedGhostButtonClass = cn('border-slate-200 bg-white/90 hover:bg-slate-50 dark:border-white/10 dark:bg-slate-950/80 dark:hover:bg-slate-900', 'hover:border-current', themeIconColor);
|
|
147
143
|
const runContextParallelInitTest = () => __awaiter(this, void 0, void 0, function* () {
|
|
148
|
-
const debugFingerprintId = activeDebugFingerprintId;
|
|
144
|
+
const debugFingerprintId = activeDebugFingerprintId !== null && activeDebugFingerprintId !== void 0 ? activeDebugFingerprintId : getOrCreateDebugFingerprintOverride();
|
|
149
145
|
if (!debugFingerprintId) {
|
|
150
146
|
setTestResult('Test fingerprint override is not ready yet.');
|
|
151
147
|
return;
|
|
152
148
|
}
|
|
149
|
+
setActiveDebugFingerprintId(debugFingerprintId);
|
|
153
150
|
setIsRunningTest(true);
|
|
154
151
|
setTestResult(`Running Frontend Prevention Test with fingerprint: ${debugFingerprintId}`);
|
|
155
152
|
try {
|
|
@@ -190,11 +187,12 @@ function FingerprintStatus() {
|
|
|
190
187
|
}
|
|
191
188
|
});
|
|
192
189
|
const runRawParallelPostTest = () => __awaiter(this, void 0, void 0, function* () {
|
|
193
|
-
const normalizedFingerprintId = activeDebugFingerprintId;
|
|
190
|
+
const normalizedFingerprintId = activeDebugFingerprintId !== null && activeDebugFingerprintId !== void 0 ? activeDebugFingerprintId : getOrCreateDebugFingerprintOverride();
|
|
194
191
|
if (!normalizedFingerprintId) {
|
|
195
192
|
setTestResult('Test fingerprint override is not ready yet.');
|
|
196
193
|
return;
|
|
197
194
|
}
|
|
195
|
+
setActiveDebugFingerprintId(normalizedFingerprintId);
|
|
198
196
|
setIsRunningTest(true);
|
|
199
197
|
setTestResult(`Running Backend Idempotency Test with fingerprint: ${normalizedFingerprintId}`);
|
|
200
198
|
try {
|
|
@@ -243,8 +241,7 @@ function FingerprintStatus() {
|
|
|
243
241
|
}
|
|
244
242
|
});
|
|
245
243
|
const regenerateTestFingerprint = () => {
|
|
246
|
-
const nextFingerprintId =
|
|
247
|
-
setDebugFingerprintOverride(nextFingerprintId);
|
|
244
|
+
const nextFingerprintId = regenerateDebugFingerprintOverride();
|
|
248
245
|
setActiveDebugFingerprintId(nextFingerprintId);
|
|
249
246
|
setTestResult(`Generated test fingerprint override: ${nextFingerprintId}`);
|
|
250
247
|
};
|
|
@@ -336,11 +333,6 @@ function StatusTag({ value }) {
|
|
|
336
333
|
const badgeClass = colorMap[normalized] || defaultColor;
|
|
337
334
|
return (jsx("span", { className: cn('inline-block rounded-full px-2 py-0.5 text-xs capitalize font-medium', badgeClass), children: value }));
|
|
338
335
|
}
|
|
339
|
-
function buildDebugFingerprintId() {
|
|
340
|
-
const timestamp = new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14);
|
|
341
|
-
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
342
|
-
return `fp_test_dbg_${timestamp}_${randomSuffix}`;
|
|
343
|
-
}
|
|
344
336
|
function formatErrorMessage(error) {
|
|
345
337
|
if (error instanceof Error) {
|
|
346
338
|
return error.message;
|
|
@@ -10,11 +10,13 @@ export declare const FINGERPRINT_SOURCE_REFER = "x-source-ref";
|
|
|
10
10
|
export declare const FINGERPRINT_FIRST_TOUCH_STORAGE_KEY = "__x_first_touch";
|
|
11
11
|
export declare const FINGERPRINT_FIRST_TOUCH_COOKIE_NAME = "__x_first_touch";
|
|
12
12
|
export declare const FINGERPRINT_FIRST_TOUCH_HEADER = "x-first-touch";
|
|
13
|
+
export declare const FINGERPRINT_DEBUG_PREFIX = "fp_test_dbg_";
|
|
13
14
|
/**
|
|
14
15
|
* 验证fingerprint ID格式
|
|
15
16
|
* 可以在客户端和服务端使用
|
|
16
17
|
*/
|
|
17
18
|
export declare function isValidFingerprintId(fingerprintId: string): boolean;
|
|
19
|
+
export declare function isDebugFingerprintId(fingerprintId: string | null | undefined): boolean;
|
|
18
20
|
export declare const FINGERPRINT_CONSTANTS: {
|
|
19
21
|
readonly STORAGE_KEY: "__x_fingerprint_id";
|
|
20
22
|
readonly DEBUG_OVERRIDE_STORAGE_KEY: "__x_fingerprint_debug_override";
|
|
@@ -23,4 +25,5 @@ export declare const FINGERPRINT_CONSTANTS: {
|
|
|
23
25
|
readonly FIRST_TOUCH_STORAGE_KEY: "__x_first_touch";
|
|
24
26
|
readonly FIRST_TOUCH_COOKIE_NAME: "__x_first_touch";
|
|
25
27
|
readonly FIRST_TOUCH_HEADER: "x-first-touch";
|
|
28
|
+
readonly DEBUG_PREFIX: "fp_test_dbg_";
|
|
26
29
|
};
|
|
@@ -13,6 +13,7 @@ const FINGERPRINT_SOURCE_REFER = 'x-source-ref';
|
|
|
13
13
|
const FINGERPRINT_FIRST_TOUCH_STORAGE_KEY = '__x_first_touch';
|
|
14
14
|
const FINGERPRINT_FIRST_TOUCH_COOKIE_NAME = '__x_first_touch';
|
|
15
15
|
const FINGERPRINT_FIRST_TOUCH_HEADER = 'x-first-touch';
|
|
16
|
+
const FINGERPRINT_DEBUG_PREFIX = 'fp_test_dbg_';
|
|
16
17
|
/**
|
|
17
18
|
* 验证fingerprint ID格式
|
|
18
19
|
* 可以在客户端和服务端使用
|
|
@@ -24,7 +25,14 @@ function isValidFingerprintId(fingerprintId) {
|
|
|
24
25
|
// - fp_ + FingerprintJS visitorId (变长字符串)
|
|
25
26
|
// - fp_fallback_ + 时间戳_随机字符串 (客户端降级方案)
|
|
26
27
|
// - fp_server_ + 时间戳_随机字符串 (服务端降级)
|
|
27
|
-
|
|
28
|
+
// - fp_test_dbg_ + 时间戳_随机字符串 (调试并发测试)
|
|
29
|
+
return /^fp(_fallback|_server|_test_dbg)?_[a-zA-Z0-9_]+$/.test(fingerprintId);
|
|
30
|
+
}
|
|
31
|
+
function isDebugFingerprintId(fingerprintId) {
|
|
32
|
+
if (!fingerprintId) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return fingerprintId.startsWith(FINGERPRINT_DEBUG_PREFIX);
|
|
28
36
|
}
|
|
29
37
|
// 常量导出
|
|
30
38
|
const FINGERPRINT_CONSTANTS = {
|
|
@@ -35,15 +43,18 @@ const FINGERPRINT_CONSTANTS = {
|
|
|
35
43
|
FIRST_TOUCH_STORAGE_KEY: FINGERPRINT_FIRST_TOUCH_STORAGE_KEY,
|
|
36
44
|
FIRST_TOUCH_COOKIE_NAME: FINGERPRINT_FIRST_TOUCH_COOKIE_NAME,
|
|
37
45
|
FIRST_TOUCH_HEADER: FINGERPRINT_FIRST_TOUCH_HEADER,
|
|
46
|
+
DEBUG_PREFIX: FINGERPRINT_DEBUG_PREFIX,
|
|
38
47
|
};
|
|
39
48
|
|
|
40
49
|
exports.FINGERPRINT_CONSTANTS = FINGERPRINT_CONSTANTS;
|
|
41
50
|
exports.FINGERPRINT_COOKIE_NAME = FINGERPRINT_COOKIE_NAME;
|
|
42
51
|
exports.FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY = FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY;
|
|
52
|
+
exports.FINGERPRINT_DEBUG_PREFIX = FINGERPRINT_DEBUG_PREFIX;
|
|
43
53
|
exports.FINGERPRINT_FIRST_TOUCH_COOKIE_NAME = FINGERPRINT_FIRST_TOUCH_COOKIE_NAME;
|
|
44
54
|
exports.FINGERPRINT_FIRST_TOUCH_HEADER = FINGERPRINT_FIRST_TOUCH_HEADER;
|
|
45
55
|
exports.FINGERPRINT_FIRST_TOUCH_STORAGE_KEY = FINGERPRINT_FIRST_TOUCH_STORAGE_KEY;
|
|
46
56
|
exports.FINGERPRINT_HEADER_NAME = FINGERPRINT_HEADER_NAME;
|
|
47
57
|
exports.FINGERPRINT_SOURCE_REFER = FINGERPRINT_SOURCE_REFER;
|
|
48
58
|
exports.FINGERPRINT_STORAGE_KEY = FINGERPRINT_STORAGE_KEY;
|
|
59
|
+
exports.isDebugFingerprintId = isDebugFingerprintId;
|
|
49
60
|
exports.isValidFingerprintId = isValidFingerprintId;
|
|
@@ -11,6 +11,7 @@ const FINGERPRINT_SOURCE_REFER = 'x-source-ref';
|
|
|
11
11
|
const FINGERPRINT_FIRST_TOUCH_STORAGE_KEY = '__x_first_touch';
|
|
12
12
|
const FINGERPRINT_FIRST_TOUCH_COOKIE_NAME = '__x_first_touch';
|
|
13
13
|
const FINGERPRINT_FIRST_TOUCH_HEADER = 'x-first-touch';
|
|
14
|
+
const FINGERPRINT_DEBUG_PREFIX = 'fp_test_dbg_';
|
|
14
15
|
/**
|
|
15
16
|
* 验证fingerprint ID格式
|
|
16
17
|
* 可以在客户端和服务端使用
|
|
@@ -22,7 +23,14 @@ function isValidFingerprintId(fingerprintId) {
|
|
|
22
23
|
// - fp_ + FingerprintJS visitorId (变长字符串)
|
|
23
24
|
// - fp_fallback_ + 时间戳_随机字符串 (客户端降级方案)
|
|
24
25
|
// - fp_server_ + 时间戳_随机字符串 (服务端降级)
|
|
25
|
-
|
|
26
|
+
// - fp_test_dbg_ + 时间戳_随机字符串 (调试并发测试)
|
|
27
|
+
return /^fp(_fallback|_server|_test_dbg)?_[a-zA-Z0-9_]+$/.test(fingerprintId);
|
|
28
|
+
}
|
|
29
|
+
function isDebugFingerprintId(fingerprintId) {
|
|
30
|
+
if (!fingerprintId) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return fingerprintId.startsWith(FINGERPRINT_DEBUG_PREFIX);
|
|
26
34
|
}
|
|
27
35
|
// 常量导出
|
|
28
36
|
const FINGERPRINT_CONSTANTS = {
|
|
@@ -33,6 +41,7 @@ const FINGERPRINT_CONSTANTS = {
|
|
|
33
41
|
FIRST_TOUCH_STORAGE_KEY: FINGERPRINT_FIRST_TOUCH_STORAGE_KEY,
|
|
34
42
|
FIRST_TOUCH_COOKIE_NAME: FINGERPRINT_FIRST_TOUCH_COOKIE_NAME,
|
|
35
43
|
FIRST_TOUCH_HEADER: FINGERPRINT_FIRST_TOUCH_HEADER,
|
|
44
|
+
DEBUG_PREFIX: FINGERPRINT_DEBUG_PREFIX,
|
|
36
45
|
};
|
|
37
46
|
|
|
38
|
-
export { FINGERPRINT_CONSTANTS, FINGERPRINT_COOKIE_NAME, FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY, FINGERPRINT_FIRST_TOUCH_COOKIE_NAME, FINGERPRINT_FIRST_TOUCH_HEADER, FINGERPRINT_FIRST_TOUCH_STORAGE_KEY, FINGERPRINT_HEADER_NAME, FINGERPRINT_SOURCE_REFER, FINGERPRINT_STORAGE_KEY, isValidFingerprintId };
|
|
47
|
+
export { FINGERPRINT_CONSTANTS, FINGERPRINT_COOKIE_NAME, FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY, FINGERPRINT_DEBUG_PREFIX, FINGERPRINT_FIRST_TOUCH_COOKIE_NAME, FINGERPRINT_FIRST_TOUCH_HEADER, FINGERPRINT_FIRST_TOUCH_STORAGE_KEY, FINGERPRINT_HEADER_NAME, FINGERPRINT_SOURCE_REFER, FINGERPRINT_STORAGE_KEY, isDebugFingerprintId, isValidFingerprintId };
|
|
@@ -11,12 +11,14 @@ var fingerprintProvider = require('./fingerprint-provider.js');
|
|
|
11
11
|
exports.FINGERPRINT_CONSTANTS = fingerprintShared.FINGERPRINT_CONSTANTS;
|
|
12
12
|
exports.FINGERPRINT_COOKIE_NAME = fingerprintShared.FINGERPRINT_COOKIE_NAME;
|
|
13
13
|
exports.FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY = fingerprintShared.FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY;
|
|
14
|
+
exports.FINGERPRINT_DEBUG_PREFIX = fingerprintShared.FINGERPRINT_DEBUG_PREFIX;
|
|
14
15
|
exports.FINGERPRINT_FIRST_TOUCH_COOKIE_NAME = fingerprintShared.FINGERPRINT_FIRST_TOUCH_COOKIE_NAME;
|
|
15
16
|
exports.FINGERPRINT_FIRST_TOUCH_HEADER = fingerprintShared.FINGERPRINT_FIRST_TOUCH_HEADER;
|
|
16
17
|
exports.FINGERPRINT_FIRST_TOUCH_STORAGE_KEY = fingerprintShared.FINGERPRINT_FIRST_TOUCH_STORAGE_KEY;
|
|
17
18
|
exports.FINGERPRINT_HEADER_NAME = fingerprintShared.FINGERPRINT_HEADER_NAME;
|
|
18
19
|
exports.FINGERPRINT_SOURCE_REFER = fingerprintShared.FINGERPRINT_SOURCE_REFER;
|
|
19
20
|
exports.FINGERPRINT_STORAGE_KEY = fingerprintShared.FINGERPRINT_STORAGE_KEY;
|
|
21
|
+
exports.isDebugFingerprintId = fingerprintShared.isDebugFingerprintId;
|
|
20
22
|
exports.isValidFingerprintId = fingerprintShared.isValidFingerprintId;
|
|
21
23
|
exports.clearFingerprintId = fingerprintClient.clearFingerprintId;
|
|
22
24
|
exports.createFingerprintFetch = fingerprintClient.createFingerprintFetch;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
export { FINGERPRINT_CONSTANTS, FINGERPRINT_COOKIE_NAME, FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY, FINGERPRINT_FIRST_TOUCH_COOKIE_NAME, FINGERPRINT_FIRST_TOUCH_HEADER, FINGERPRINT_FIRST_TOUCH_STORAGE_KEY, FINGERPRINT_HEADER_NAME, FINGERPRINT_SOURCE_REFER, FINGERPRINT_STORAGE_KEY, isValidFingerprintId } from './fingerprint-shared.mjs';
|
|
2
|
+
export { FINGERPRINT_CONSTANTS, FINGERPRINT_COOKIE_NAME, FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY, FINGERPRINT_DEBUG_PREFIX, FINGERPRINT_FIRST_TOUCH_COOKIE_NAME, FINGERPRINT_FIRST_TOUCH_HEADER, FINGERPRINT_FIRST_TOUCH_STORAGE_KEY, FINGERPRINT_HEADER_NAME, FINGERPRINT_SOURCE_REFER, FINGERPRINT_STORAGE_KEY, isDebugFingerprintId, isValidFingerprintId } from './fingerprint-shared.mjs';
|
|
3
3
|
export { clearFingerprintId, createFingerprintFetch, createFingerprintHeaders, generateFingerprintId, getFingerprintId, getOrCreateFirstTouchData, getOrGenerateFingerprintId, setFingerprintId, useFingerprintHeaders } from './fingerprint-client.mjs';
|
|
4
4
|
export { useFingerprint } from './use-fingerprint.mjs';
|
|
5
5
|
export { FingerprintProvider, FingerprintStatus, useFingerprintContext, useFingerprintContextSafe, withFingerprint } from './fingerprint-provider.mjs';
|
|
@@ -8,12 +8,14 @@ var fingerprintServer = require('./fingerprint-server.js');
|
|
|
8
8
|
exports.FINGERPRINT_CONSTANTS = fingerprintShared.FINGERPRINT_CONSTANTS;
|
|
9
9
|
exports.FINGERPRINT_COOKIE_NAME = fingerprintShared.FINGERPRINT_COOKIE_NAME;
|
|
10
10
|
exports.FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY = fingerprintShared.FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY;
|
|
11
|
+
exports.FINGERPRINT_DEBUG_PREFIX = fingerprintShared.FINGERPRINT_DEBUG_PREFIX;
|
|
11
12
|
exports.FINGERPRINT_FIRST_TOUCH_COOKIE_NAME = fingerprintShared.FINGERPRINT_FIRST_TOUCH_COOKIE_NAME;
|
|
12
13
|
exports.FINGERPRINT_FIRST_TOUCH_HEADER = fingerprintShared.FINGERPRINT_FIRST_TOUCH_HEADER;
|
|
13
14
|
exports.FINGERPRINT_FIRST_TOUCH_STORAGE_KEY = fingerprintShared.FINGERPRINT_FIRST_TOUCH_STORAGE_KEY;
|
|
14
15
|
exports.FINGERPRINT_HEADER_NAME = fingerprintShared.FINGERPRINT_HEADER_NAME;
|
|
15
16
|
exports.FINGERPRINT_SOURCE_REFER = fingerprintShared.FINGERPRINT_SOURCE_REFER;
|
|
16
17
|
exports.FINGERPRINT_STORAGE_KEY = fingerprintShared.FINGERPRINT_STORAGE_KEY;
|
|
18
|
+
exports.isDebugFingerprintId = fingerprintShared.isDebugFingerprintId;
|
|
17
19
|
exports.isValidFingerprintId = fingerprintShared.isValidFingerprintId;
|
|
18
20
|
exports.extractFingerprintFromNextRequest = fingerprintServer.extractFingerprintFromNextRequest;
|
|
19
21
|
exports.extractFingerprintFromNextStores = fingerprintServer.extractFingerprintFromNextStores;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { FINGERPRINT_CONSTANTS, FINGERPRINT_COOKIE_NAME, FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY, FINGERPRINT_FIRST_TOUCH_COOKIE_NAME, FINGERPRINT_FIRST_TOUCH_HEADER, FINGERPRINT_FIRST_TOUCH_STORAGE_KEY, FINGERPRINT_HEADER_NAME, FINGERPRINT_SOURCE_REFER, FINGERPRINT_STORAGE_KEY, isValidFingerprintId } from './fingerprint-shared.mjs';
|
|
1
|
+
export { FINGERPRINT_CONSTANTS, FINGERPRINT_COOKIE_NAME, FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY, FINGERPRINT_DEBUG_PREFIX, FINGERPRINT_FIRST_TOUCH_COOKIE_NAME, FINGERPRINT_FIRST_TOUCH_HEADER, FINGERPRINT_FIRST_TOUCH_STORAGE_KEY, FINGERPRINT_HEADER_NAME, FINGERPRINT_SOURCE_REFER, FINGERPRINT_STORAGE_KEY, isDebugFingerprintId, isValidFingerprintId } from './fingerprint-shared.mjs';
|
|
2
2
|
export { extractFingerprintFromNextRequest, extractFingerprintFromNextStores, extractFingerprintId, generateServerFingerprintId } from './fingerprint-server.mjs';
|
|
@@ -99,8 +99,13 @@ function useFingerprint(config) {
|
|
|
99
99
|
setXCredit(data.xCredit || null);
|
|
100
100
|
setXSubscription(data.xSubscription || null);
|
|
101
101
|
setIsInitialized(true);
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
const canonicalFingerprintId = (_a = data.xUser) === null || _a === void 0 ? void 0 : _a.fingerprintId;
|
|
103
|
+
if (canonicalFingerprintId &&
|
|
104
|
+
fingerprintShared.isValidFingerprintId(canonicalFingerprintId) &&
|
|
105
|
+
!fingerprintShared.isDebugFingerprintId(canonicalFingerprintId) &&
|
|
106
|
+
canonicalFingerprintId !== fingerprintId) {
|
|
107
|
+
setFingerprintIdState(canonicalFingerprintId);
|
|
108
|
+
fingerprintClient.setFingerprintId(canonicalFingerprintId);
|
|
104
109
|
}
|
|
105
110
|
}
|
|
106
111
|
else {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { __awaiter } from '../../node_modules/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.46.2_tslib@2.8.1_typescript@5.9.3/node_modules/tslib/tslib.es6.mjs';
|
|
3
3
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
4
|
-
import { getOrCreateFirstTouchData, getOrGenerateFingerprintId, createFingerprintHeaders } from './fingerprint-client.mjs';
|
|
5
|
-
import { FINGERPRINT_SOURCE_REFER } from './fingerprint-shared.mjs';
|
|
4
|
+
import { getOrCreateFirstTouchData, getOrGenerateFingerprintId, createFingerprintHeaders, setFingerprintId } from './fingerprint-client.mjs';
|
|
5
|
+
import { FINGERPRINT_SOURCE_REFER, isValidFingerprintId, isDebugFingerprintId } from './fingerprint-shared.mjs';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Hook for managing fingerprint ID and anonymous user data
|
|
@@ -97,8 +97,13 @@ function useFingerprint(config) {
|
|
|
97
97
|
setXCredit(data.xCredit || null);
|
|
98
98
|
setXSubscription(data.xSubscription || null);
|
|
99
99
|
setIsInitialized(true);
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
const canonicalFingerprintId = (_a = data.xUser) === null || _a === void 0 ? void 0 : _a.fingerprintId;
|
|
101
|
+
if (canonicalFingerprintId &&
|
|
102
|
+
isValidFingerprintId(canonicalFingerprintId) &&
|
|
103
|
+
!isDebugFingerprintId(canonicalFingerprintId) &&
|
|
104
|
+
canonicalFingerprintId !== fingerprintId) {
|
|
105
|
+
setFingerprintIdState(canonicalFingerprintId);
|
|
106
|
+
setFingerprintId(canonicalFingerprintId);
|
|
102
107
|
}
|
|
103
108
|
}
|
|
104
109
|
else {
|
|
@@ -45,6 +45,10 @@ const CLERK_ACTIVE_ANIMATION_EASING = 'cubic-bezier(0.22, 1, 0.36, 1)';
|
|
|
45
45
|
const CLERK_TEXT_GAP_FROM_PATH = 12;
|
|
46
46
|
// Radius of numbered step badges rendered on top of the path centerline.
|
|
47
47
|
const CLERK_STEP_BADGE_RADIUS = 7;
|
|
48
|
+
// Visual line offsets by grouped heading depth: 1/2, 3, 4, >4.
|
|
49
|
+
const CLERK_DEPTH_GROUP_LINE_OFFSETS = [6, 18, 30, 42];
|
|
50
|
+
// Max number of characters rendered for a TOC label before trimming with ellipsis.
|
|
51
|
+
const CLERK_MAX_LABEL_LENGTH = 44;
|
|
48
52
|
function PortableClerkTOC({ toc, header, footer, title, emptyLabel = 'No headings', className, }) {
|
|
49
53
|
return (jsxRuntime.jsxs(page.PageTOC, { className: className, children: [header, title !== null && title !== void 0 ? title : jsxRuntime.jsx(page.PageTOCTitle, {}), jsxRuntime.jsx(PortableClerkTOCScrollArea, { children: jsxRuntime.jsx(PortableClerkTOCItems, { toc: toc, emptyLabel: emptyLabel }) }), footer] }));
|
|
50
54
|
}
|
|
@@ -124,16 +128,16 @@ function PortableClerkTOCItems(_a) {
|
|
|
124
128
|
if (toc.length === 0) {
|
|
125
129
|
return (jsxRuntime.jsx("div", { className: "rounded-lg border bg-fd-card p-3 text-xs text-fd-muted-foreground", children: emptyLabel }));
|
|
126
130
|
}
|
|
127
|
-
return (jsxRuntime.jsxs("div", Object.assign({ ref: mergeRefs(containerRef, ref), className: cn('relative flex flex-col', className) }, props, { children: [jsxRuntime.jsx(ClerkOutline, { path: outlinePath, items: layout.items, activePath: activePath, activeAnchors: activeAnchors, activeEndpoint: activeEndpoint }), metas.map((meta, i) => (jsxRuntime.jsx(PortableClerkTOCItem, { item: meta.item, isActive: activeAnchors.includes(meta.item.url.slice(1)), resolvedContent: meta.resolvedContent, itemPadding: meta.itemPadding, contentRef: (node) => {
|
|
131
|
+
return (jsxRuntime.jsxs("div", Object.assign({ ref: mergeRefs(containerRef, ref), className: cn('relative flex flex-col', className) }, props, { children: [jsxRuntime.jsx(ClerkOutline, { path: outlinePath, items: layout.items, activePath: activePath, activeAnchors: activeAnchors, activeEndpoint: activeEndpoint }), metas.map((meta, i) => (jsxRuntime.jsx(PortableClerkTOCItem, { item: meta.item, isActive: activeAnchors.includes(meta.item.url.slice(1)), resolvedContent: meta.resolvedContent, fullTitle: meta.fullTitle, itemPadding: meta.itemPadding, contentRef: (node) => {
|
|
128
132
|
contentRefs.current[i] = node;
|
|
129
133
|
}, ref: (node) => {
|
|
130
134
|
itemRefs.current[i] = node;
|
|
131
135
|
} }, meta.item.url)))] })));
|
|
132
136
|
}
|
|
133
|
-
function PortableClerkTOCItem({ item, isActive, resolvedContent, itemPadding, contentRef, ref, }) {
|
|
134
|
-
return (jsxRuntime.jsx(Primitive__namespace.TOCItem, { ref: ref, href: item.url, "data-clerk-item": "", style: {
|
|
137
|
+
function PortableClerkTOCItem({ item, isActive, resolvedContent, fullTitle, itemPadding, contentRef, ref, }) {
|
|
138
|
+
return (jsxRuntime.jsx(Primitive__namespace.TOCItem, { ref: ref, href: item.url, "data-clerk-item": "", title: fullTitle !== null && fullTitle !== void 0 ? fullTitle : undefined, style: {
|
|
135
139
|
paddingInlineStart: itemPadding,
|
|
136
|
-
}, className: cn('prose group relative py-1.5 text-sm transition-colors
|
|
140
|
+
}, className: cn('prose group relative py-1.5 text-sm transition-colors first:pt-0 last:pb-0 hover:text-fd-accent-foreground', isActive ? lib.themeIconColor : 'text-fd-muted-foreground'), children: jsxRuntime.jsx("span", { ref: contentRef, className: "relative z-10 block overflow-hidden text-ellipsis whitespace-nowrap", children: resolvedContent }) }));
|
|
137
141
|
}
|
|
138
142
|
function ClerkOutline({ path, items, activePath, activeAnchors, activeEndpoint, }) {
|
|
139
143
|
const activeSet = new Set(activeAnchors);
|
|
@@ -176,19 +180,33 @@ function getItemOffset(depth) {
|
|
|
176
180
|
return lineOffset + badgeRadius + CLERK_TEXT_GAP_FROM_PATH;
|
|
177
181
|
}
|
|
178
182
|
function getLineOffset(depth) {
|
|
179
|
-
|
|
183
|
+
const group = getDepthGroup(depth);
|
|
184
|
+
return CLERK_DEPTH_GROUP_LINE_OFFSETS[group];
|
|
180
185
|
}
|
|
181
186
|
function getVisualLinePosition(depth) {
|
|
182
187
|
return getLineOffset(depth);
|
|
183
188
|
}
|
|
189
|
+
function getDepthGroup(depth) {
|
|
190
|
+
if (depth <= 2)
|
|
191
|
+
return 0;
|
|
192
|
+
if (depth === 3)
|
|
193
|
+
return 1;
|
|
194
|
+
if (depth === 4)
|
|
195
|
+
return 2;
|
|
196
|
+
return 3;
|
|
197
|
+
}
|
|
184
198
|
function resolveClerkItem(item) {
|
|
185
199
|
const isH3 = item.depth === 3;
|
|
186
200
|
const rawTitle = typeof item.title === 'string' ? item.title : '';
|
|
187
201
|
const { isStep, displayStep, content } = getStepInfoFromTitle(rawTitle);
|
|
188
202
|
let stepNumber = isH3 && isStep ? String(displayStep) : null;
|
|
189
203
|
let resolvedContent = item.title;
|
|
204
|
+
let fullTitle = rawTitle || null;
|
|
190
205
|
if (isH3 && isStep) {
|
|
191
206
|
resolvedContent = content !== null && content !== void 0 ? content : item.title;
|
|
207
|
+
if (typeof content === 'string') {
|
|
208
|
+
fullTitle = content;
|
|
209
|
+
}
|
|
192
210
|
}
|
|
193
211
|
if (isH3 && !stepNumber) {
|
|
194
212
|
const urlNum = getDigitsFromUrl(item.url);
|
|
@@ -199,18 +217,30 @@ function resolveClerkItem(item) {
|
|
|
199
217
|
const match = rawTitle.match(/^(\d+(?:\.\d+)*\.?)\s+(.+)$/);
|
|
200
218
|
if (match === null || match === void 0 ? void 0 : match[2]) {
|
|
201
219
|
resolvedContent = match[2];
|
|
220
|
+
fullTitle = match[2];
|
|
202
221
|
}
|
|
203
222
|
}
|
|
204
223
|
}
|
|
205
224
|
}
|
|
225
|
+
if (typeof resolvedContent === 'string') {
|
|
226
|
+
fullTitle = resolvedContent;
|
|
227
|
+
resolvedContent = truncateClerkLabel(resolvedContent);
|
|
228
|
+
}
|
|
206
229
|
return {
|
|
207
230
|
item,
|
|
208
231
|
resolvedContent,
|
|
232
|
+
fullTitle,
|
|
209
233
|
stepNumber,
|
|
210
234
|
itemPadding: getItemOffset(item.depth),
|
|
211
235
|
lineOffset: getVisualLinePosition(item.depth),
|
|
212
236
|
};
|
|
213
237
|
}
|
|
238
|
+
function truncateClerkLabel(value) {
|
|
239
|
+
const normalized = value.trim();
|
|
240
|
+
if (normalized.length <= CLERK_MAX_LABEL_LENGTH)
|
|
241
|
+
return normalized;
|
|
242
|
+
return `${normalized.slice(0, CLERK_MAX_LABEL_LENGTH).trimEnd()}...`;
|
|
243
|
+
}
|
|
214
244
|
function buildOutlinePath(items) {
|
|
215
245
|
if (items.length === 0)
|
|
216
246
|
return '';
|
|
@@ -24,6 +24,10 @@ const CLERK_ACTIVE_ANIMATION_EASING = 'cubic-bezier(0.22, 1, 0.36, 1)';
|
|
|
24
24
|
const CLERK_TEXT_GAP_FROM_PATH = 12;
|
|
25
25
|
// Radius of numbered step badges rendered on top of the path centerline.
|
|
26
26
|
const CLERK_STEP_BADGE_RADIUS = 7;
|
|
27
|
+
// Visual line offsets by grouped heading depth: 1/2, 3, 4, >4.
|
|
28
|
+
const CLERK_DEPTH_GROUP_LINE_OFFSETS = [6, 18, 30, 42];
|
|
29
|
+
// Max number of characters rendered for a TOC label before trimming with ellipsis.
|
|
30
|
+
const CLERK_MAX_LABEL_LENGTH = 44;
|
|
27
31
|
function PortableClerkTOC({ toc, header, footer, title, emptyLabel = 'No headings', className, }) {
|
|
28
32
|
return (jsxs(PageTOC, { className: className, children: [header, title !== null && title !== void 0 ? title : jsx(PageTOCTitle, {}), jsx(PortableClerkTOCScrollArea, { children: jsx(PortableClerkTOCItems, { toc: toc, emptyLabel: emptyLabel }) }), footer] }));
|
|
29
33
|
}
|
|
@@ -103,16 +107,16 @@ function PortableClerkTOCItems(_a) {
|
|
|
103
107
|
if (toc.length === 0) {
|
|
104
108
|
return (jsx("div", { className: "rounded-lg border bg-fd-card p-3 text-xs text-fd-muted-foreground", children: emptyLabel }));
|
|
105
109
|
}
|
|
106
|
-
return (jsxs("div", Object.assign({ ref: mergeRefs(containerRef, ref), className: cn('relative flex flex-col', className) }, props, { children: [jsx(ClerkOutline, { path: outlinePath, items: layout.items, activePath: activePath, activeAnchors: activeAnchors, activeEndpoint: activeEndpoint }), metas.map((meta, i) => (jsx(PortableClerkTOCItem, { item: meta.item, isActive: activeAnchors.includes(meta.item.url.slice(1)), resolvedContent: meta.resolvedContent, itemPadding: meta.itemPadding, contentRef: (node) => {
|
|
110
|
+
return (jsxs("div", Object.assign({ ref: mergeRefs(containerRef, ref), className: cn('relative flex flex-col', className) }, props, { children: [jsx(ClerkOutline, { path: outlinePath, items: layout.items, activePath: activePath, activeAnchors: activeAnchors, activeEndpoint: activeEndpoint }), metas.map((meta, i) => (jsx(PortableClerkTOCItem, { item: meta.item, isActive: activeAnchors.includes(meta.item.url.slice(1)), resolvedContent: meta.resolvedContent, fullTitle: meta.fullTitle, itemPadding: meta.itemPadding, contentRef: (node) => {
|
|
107
111
|
contentRefs.current[i] = node;
|
|
108
112
|
}, ref: (node) => {
|
|
109
113
|
itemRefs.current[i] = node;
|
|
110
114
|
} }, meta.item.url)))] })));
|
|
111
115
|
}
|
|
112
|
-
function PortableClerkTOCItem({ item, isActive, resolvedContent, itemPadding, contentRef, ref, }) {
|
|
113
|
-
return (jsx(Primitive.TOCItem, { ref: ref, href: item.url, "data-clerk-item": "", style: {
|
|
116
|
+
function PortableClerkTOCItem({ item, isActive, resolvedContent, fullTitle, itemPadding, contentRef, ref, }) {
|
|
117
|
+
return (jsx(Primitive.TOCItem, { ref: ref, href: item.url, "data-clerk-item": "", title: fullTitle !== null && fullTitle !== void 0 ? fullTitle : undefined, style: {
|
|
114
118
|
paddingInlineStart: itemPadding,
|
|
115
|
-
}, className: cn('prose group relative py-1.5 text-sm transition-colors
|
|
119
|
+
}, className: cn('prose group relative py-1.5 text-sm transition-colors first:pt-0 last:pb-0 hover:text-fd-accent-foreground', isActive ? themeIconColor : 'text-fd-muted-foreground'), children: jsx("span", { ref: contentRef, className: "relative z-10 block overflow-hidden text-ellipsis whitespace-nowrap", children: resolvedContent }) }));
|
|
116
120
|
}
|
|
117
121
|
function ClerkOutline({ path, items, activePath, activeAnchors, activeEndpoint, }) {
|
|
118
122
|
const activeSet = new Set(activeAnchors);
|
|
@@ -155,19 +159,33 @@ function getItemOffset(depth) {
|
|
|
155
159
|
return lineOffset + badgeRadius + CLERK_TEXT_GAP_FROM_PATH;
|
|
156
160
|
}
|
|
157
161
|
function getLineOffset(depth) {
|
|
158
|
-
|
|
162
|
+
const group = getDepthGroup(depth);
|
|
163
|
+
return CLERK_DEPTH_GROUP_LINE_OFFSETS[group];
|
|
159
164
|
}
|
|
160
165
|
function getVisualLinePosition(depth) {
|
|
161
166
|
return getLineOffset(depth);
|
|
162
167
|
}
|
|
168
|
+
function getDepthGroup(depth) {
|
|
169
|
+
if (depth <= 2)
|
|
170
|
+
return 0;
|
|
171
|
+
if (depth === 3)
|
|
172
|
+
return 1;
|
|
173
|
+
if (depth === 4)
|
|
174
|
+
return 2;
|
|
175
|
+
return 3;
|
|
176
|
+
}
|
|
163
177
|
function resolveClerkItem(item) {
|
|
164
178
|
const isH3 = item.depth === 3;
|
|
165
179
|
const rawTitle = typeof item.title === 'string' ? item.title : '';
|
|
166
180
|
const { isStep, displayStep, content } = getStepInfoFromTitle(rawTitle);
|
|
167
181
|
let stepNumber = isH3 && isStep ? String(displayStep) : null;
|
|
168
182
|
let resolvedContent = item.title;
|
|
183
|
+
let fullTitle = rawTitle || null;
|
|
169
184
|
if (isH3 && isStep) {
|
|
170
185
|
resolvedContent = content !== null && content !== void 0 ? content : item.title;
|
|
186
|
+
if (typeof content === 'string') {
|
|
187
|
+
fullTitle = content;
|
|
188
|
+
}
|
|
171
189
|
}
|
|
172
190
|
if (isH3 && !stepNumber) {
|
|
173
191
|
const urlNum = getDigitsFromUrl(item.url);
|
|
@@ -178,18 +196,30 @@ function resolveClerkItem(item) {
|
|
|
178
196
|
const match = rawTitle.match(/^(\d+(?:\.\d+)*\.?)\s+(.+)$/);
|
|
179
197
|
if (match === null || match === void 0 ? void 0 : match[2]) {
|
|
180
198
|
resolvedContent = match[2];
|
|
199
|
+
fullTitle = match[2];
|
|
181
200
|
}
|
|
182
201
|
}
|
|
183
202
|
}
|
|
184
203
|
}
|
|
204
|
+
if (typeof resolvedContent === 'string') {
|
|
205
|
+
fullTitle = resolvedContent;
|
|
206
|
+
resolvedContent = truncateClerkLabel(resolvedContent);
|
|
207
|
+
}
|
|
185
208
|
return {
|
|
186
209
|
item,
|
|
187
210
|
resolvedContent,
|
|
211
|
+
fullTitle,
|
|
188
212
|
stepNumber,
|
|
189
213
|
itemPadding: getItemOffset(item.depth),
|
|
190
214
|
lineOffset: getVisualLinePosition(item.depth),
|
|
191
215
|
};
|
|
192
216
|
}
|
|
217
|
+
function truncateClerkLabel(value) {
|
|
218
|
+
const normalized = value.trim();
|
|
219
|
+
if (normalized.length <= CLERK_MAX_LABEL_LENGTH)
|
|
220
|
+
return normalized;
|
|
221
|
+
return `${normalized.slice(0, CLERK_MAX_LABEL_LENGTH).trimEnd()}...`;
|
|
222
|
+
}
|
|
193
223
|
function buildOutlinePath(items) {
|
|
194
224
|
if (items.length === 0)
|
|
195
225
|
return '';
|
package/package.json
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
FINGERPRINT_DEBUG_PREFIX,
|
|
4
5
|
FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY,
|
|
6
|
+
isDebugFingerprintId,
|
|
5
7
|
isValidFingerprintId,
|
|
6
8
|
} from './fingerprint-shared';
|
|
7
9
|
|
|
@@ -31,7 +33,7 @@ function setLocalStorageValue(key: string, value: string): void {
|
|
|
31
33
|
|
|
32
34
|
export function getDebugFingerprintOverride(): string | null {
|
|
33
35
|
const value = getLocalStorageValue(FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY);
|
|
34
|
-
if (!value || !isValidFingerprintId(value)) {
|
|
36
|
+
if (!value || !isValidFingerprintId(value) || !isDebugFingerprintId(value)) {
|
|
35
37
|
return null;
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -39,9 +41,32 @@ export function getDebugFingerprintOverride(): string | null {
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
export function setDebugFingerprintOverride(fingerprintId: string): void {
|
|
42
|
-
if (!isValidFingerprintId(fingerprintId)) {
|
|
44
|
+
if (!isValidFingerprintId(fingerprintId) || !isDebugFingerprintId(fingerprintId)) {
|
|
43
45
|
throw new Error('Invalid fingerprint ID');
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
setLocalStorageValue(FINGERPRINT_DEBUG_OVERRIDE_STORAGE_KEY, fingerprintId);
|
|
47
49
|
}
|
|
50
|
+
|
|
51
|
+
export function buildDebugFingerprintId(): string {
|
|
52
|
+
const timestamp = new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14);
|
|
53
|
+
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
54
|
+
return `${FINGERPRINT_DEBUG_PREFIX}${timestamp}_${randomSuffix}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getOrCreateDebugFingerprintOverride(): string {
|
|
58
|
+
const existing = getDebugFingerprintOverride();
|
|
59
|
+
if (existing) {
|
|
60
|
+
return existing;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const nextFingerprintId = buildDebugFingerprintId();
|
|
64
|
+
setDebugFingerprintOverride(nextFingerprintId);
|
|
65
|
+
return nextFingerprintId;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function regenerateDebugFingerprintOverride(): string {
|
|
69
|
+
const nextFingerprintId = buildDebugFingerprintId();
|
|
70
|
+
setDebugFingerprintOverride(nextFingerprintId);
|
|
71
|
+
return nextFingerprintId;
|
|
72
|
+
}
|
|
@@ -9,8 +9,8 @@ import { useFingerprint } from './use-fingerprint';
|
|
|
9
9
|
import { CopyableText } from '@windrun-huaiin/base-ui/ui';
|
|
10
10
|
import { createFingerprintHeaders } from './fingerprint-client';
|
|
11
11
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
getOrCreateDebugFingerprintOverride,
|
|
13
|
+
regenerateDebugFingerprintOverride,
|
|
14
14
|
} from './fingerprint-debug';
|
|
15
15
|
import { FINGERPRINT_SOURCE_REFER } from './fingerprint-shared';
|
|
16
16
|
|
|
@@ -112,16 +112,12 @@ export function FingerprintStatus() {
|
|
|
112
112
|
}, [xUser]);
|
|
113
113
|
|
|
114
114
|
useEffect(() => {
|
|
115
|
-
|
|
116
|
-
if (debugFingerprintOverride) {
|
|
117
|
-
setActiveDebugFingerprintId(debugFingerprintOverride);
|
|
115
|
+
if (panelMode !== 'test') {
|
|
118
116
|
return;
|
|
119
117
|
}
|
|
120
118
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
setActiveDebugFingerprintId(nextFingerprintId);
|
|
124
|
-
}, []);
|
|
119
|
+
setActiveDebugFingerprintId((current) => current ?? getOrCreateDebugFingerprintOverride());
|
|
120
|
+
}, [panelMode]);
|
|
125
121
|
|
|
126
122
|
const creditBuckets = useMemo(() => {
|
|
127
123
|
if (!xCredit) return [];
|
|
@@ -185,12 +181,13 @@ export function FingerprintStatus() {
|
|
|
185
181
|
);
|
|
186
182
|
|
|
187
183
|
const runContextParallelInitTest = async () => {
|
|
188
|
-
const debugFingerprintId = activeDebugFingerprintId;
|
|
184
|
+
const debugFingerprintId = activeDebugFingerprintId ?? getOrCreateDebugFingerprintOverride();
|
|
189
185
|
if (!debugFingerprintId) {
|
|
190
186
|
setTestResult('Test fingerprint override is not ready yet.');
|
|
191
187
|
return;
|
|
192
188
|
}
|
|
193
189
|
|
|
190
|
+
setActiveDebugFingerprintId(debugFingerprintId);
|
|
194
191
|
setIsRunningTest(true);
|
|
195
192
|
setTestResult(`Running Frontend Prevention Test with fingerprint: ${debugFingerprintId}`);
|
|
196
193
|
|
|
@@ -240,12 +237,13 @@ export function FingerprintStatus() {
|
|
|
240
237
|
};
|
|
241
238
|
|
|
242
239
|
const runRawParallelPostTest = async () => {
|
|
243
|
-
const normalizedFingerprintId = activeDebugFingerprintId;
|
|
240
|
+
const normalizedFingerprintId = activeDebugFingerprintId ?? getOrCreateDebugFingerprintOverride();
|
|
244
241
|
if (!normalizedFingerprintId) {
|
|
245
242
|
setTestResult('Test fingerprint override is not ready yet.');
|
|
246
243
|
return;
|
|
247
244
|
}
|
|
248
245
|
|
|
246
|
+
setActiveDebugFingerprintId(normalizedFingerprintId);
|
|
249
247
|
setIsRunningTest(true);
|
|
250
248
|
setTestResult(`Running Backend Idempotency Test with fingerprint: ${normalizedFingerprintId}`);
|
|
251
249
|
|
|
@@ -303,8 +301,7 @@ export function FingerprintStatus() {
|
|
|
303
301
|
};
|
|
304
302
|
|
|
305
303
|
const regenerateTestFingerprint = () => {
|
|
306
|
-
const nextFingerprintId =
|
|
307
|
-
setDebugFingerprintOverride(nextFingerprintId);
|
|
304
|
+
const nextFingerprintId = regenerateDebugFingerprintOverride();
|
|
308
305
|
setActiveDebugFingerprintId(nextFingerprintId);
|
|
309
306
|
setTestResult(`Generated test fingerprint override: ${nextFingerprintId}`);
|
|
310
307
|
};
|
|
@@ -680,12 +677,6 @@ function StatusTag({ value }: { value: string | undefined | null }) {
|
|
|
680
677
|
);
|
|
681
678
|
}
|
|
682
679
|
|
|
683
|
-
function buildDebugFingerprintId() {
|
|
684
|
-
const timestamp = new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14);
|
|
685
|
-
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
686
|
-
return `fp_test_dbg_${timestamp}_${randomSuffix}`;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
680
|
function formatErrorMessage(error: unknown) {
|
|
690
681
|
if (error instanceof Error) {
|
|
691
682
|
return error.message;
|
|
@@ -12,6 +12,7 @@ export const FINGERPRINT_SOURCE_REFER = 'x-source-ref';
|
|
|
12
12
|
export const FINGERPRINT_FIRST_TOUCH_STORAGE_KEY = '__x_first_touch';
|
|
13
13
|
export const FINGERPRINT_FIRST_TOUCH_COOKIE_NAME = '__x_first_touch';
|
|
14
14
|
export const FINGERPRINT_FIRST_TOUCH_HEADER = 'x-first-touch';
|
|
15
|
+
export const FINGERPRINT_DEBUG_PREFIX = 'fp_test_dbg_';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* 验证fingerprint ID格式
|
|
@@ -23,7 +24,16 @@ export function isValidFingerprintId(fingerprintId: string): boolean {
|
|
|
23
24
|
// - fp_ + FingerprintJS visitorId (变长字符串)
|
|
24
25
|
// - fp_fallback_ + 时间戳_随机字符串 (客户端降级方案)
|
|
25
26
|
// - fp_server_ + 时间戳_随机字符串 (服务端降级)
|
|
26
|
-
|
|
27
|
+
// - fp_test_dbg_ + 时间戳_随机字符串 (调试并发测试)
|
|
28
|
+
return /^fp(_fallback|_server|_test_dbg)?_[a-zA-Z0-9_]+$/.test(fingerprintId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isDebugFingerprintId(fingerprintId: string | null | undefined): boolean {
|
|
32
|
+
if (!fingerprintId) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return fingerprintId.startsWith(FINGERPRINT_DEBUG_PREFIX);
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
// 常量导出
|
|
@@ -35,4 +45,5 @@ export const FINGERPRINT_CONSTANTS = {
|
|
|
35
45
|
FIRST_TOUCH_STORAGE_KEY: FINGERPRINT_FIRST_TOUCH_STORAGE_KEY,
|
|
36
46
|
FIRST_TOUCH_COOKIE_NAME: FINGERPRINT_FIRST_TOUCH_COOKIE_NAME,
|
|
37
47
|
FIRST_TOUCH_HEADER: FINGERPRINT_FIRST_TOUCH_HEADER,
|
|
48
|
+
DEBUG_PREFIX: FINGERPRINT_DEBUG_PREFIX,
|
|
38
49
|
} as const;
|
|
@@ -4,7 +4,8 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
4
4
|
import {
|
|
5
5
|
createFingerprintHeaders,
|
|
6
6
|
getOrCreateFirstTouchData,
|
|
7
|
-
getOrGenerateFingerprintId
|
|
7
|
+
getOrGenerateFingerprintId,
|
|
8
|
+
setFingerprintId,
|
|
8
9
|
} from './fingerprint-client';
|
|
9
10
|
import type {
|
|
10
11
|
FingerprintConfig,
|
|
@@ -13,7 +14,7 @@ import type {
|
|
|
13
14
|
XSubscription,
|
|
14
15
|
XUser
|
|
15
16
|
} from './types';
|
|
16
|
-
import { FINGERPRINT_SOURCE_REFER } from './fingerprint-shared'
|
|
17
|
+
import { FINGERPRINT_SOURCE_REFER, isDebugFingerprintId, isValidFingerprintId } from './fingerprint-shared'
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Hook for managing fingerprint ID and anonymous user data
|
|
@@ -121,8 +122,15 @@ export function useFingerprint(config: FingerprintConfig): UseFingerprintResult
|
|
|
121
122
|
setXSubscription(data.xSubscription || null);
|
|
122
123
|
setIsInitialized(true);
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
const canonicalFingerprintId = data.xUser?.fingerprintId;
|
|
126
|
+
if (
|
|
127
|
+
canonicalFingerprintId &&
|
|
128
|
+
isValidFingerprintId(canonicalFingerprintId) &&
|
|
129
|
+
!isDebugFingerprintId(canonicalFingerprintId) &&
|
|
130
|
+
canonicalFingerprintId !== fingerprintId
|
|
131
|
+
) {
|
|
132
|
+
setFingerprintIdState(canonicalFingerprintId);
|
|
133
|
+
setFingerprintId(canonicalFingerprintId);
|
|
126
134
|
}
|
|
127
135
|
} else {
|
|
128
136
|
throw new Error(data.error || 'Unknown error occurred');
|
|
@@ -40,6 +40,7 @@ type PortableClerkTOCProps = {
|
|
|
40
40
|
type ClerkItemMeta = {
|
|
41
41
|
item: TOCItemType;
|
|
42
42
|
resolvedContent: ReactNode;
|
|
43
|
+
fullTitle: string | null;
|
|
43
44
|
stepNumber: string | null;
|
|
44
45
|
itemPadding: number;
|
|
45
46
|
lineOffset: number;
|
|
@@ -70,6 +71,10 @@ const CLERK_ACTIVE_ANIMATION_EASING = 'cubic-bezier(0.22, 1, 0.36, 1)';
|
|
|
70
71
|
const CLERK_TEXT_GAP_FROM_PATH = 12;
|
|
71
72
|
// Radius of numbered step badges rendered on top of the path centerline.
|
|
72
73
|
const CLERK_STEP_BADGE_RADIUS = 7;
|
|
74
|
+
// Visual line offsets by grouped heading depth: 1/2, 3, 4, >4.
|
|
75
|
+
const CLERK_DEPTH_GROUP_LINE_OFFSETS = [6, 18, 30, 42] as const;
|
|
76
|
+
// Max number of characters rendered for a TOC label before trimming with ellipsis.
|
|
77
|
+
const CLERK_MAX_LABEL_LENGTH = 44;
|
|
73
78
|
|
|
74
79
|
export function PortableClerkTOC({
|
|
75
80
|
toc,
|
|
@@ -250,6 +255,7 @@ export function PortableClerkTOCItems({
|
|
|
250
255
|
item={meta.item}
|
|
251
256
|
isActive={activeAnchors.includes(meta.item.url.slice(1))}
|
|
252
257
|
resolvedContent={meta.resolvedContent}
|
|
258
|
+
fullTitle={meta.fullTitle}
|
|
253
259
|
itemPadding={meta.itemPadding}
|
|
254
260
|
contentRef={(node: HTMLSpanElement | null) => {
|
|
255
261
|
contentRefs.current[i] = node;
|
|
@@ -267,6 +273,7 @@ function PortableClerkTOCItem({
|
|
|
267
273
|
item,
|
|
268
274
|
isActive,
|
|
269
275
|
resolvedContent,
|
|
276
|
+
fullTitle,
|
|
270
277
|
itemPadding,
|
|
271
278
|
contentRef,
|
|
272
279
|
ref,
|
|
@@ -274,6 +281,7 @@ function PortableClerkTOCItem({
|
|
|
274
281
|
item: TOCItemType;
|
|
275
282
|
isActive: boolean;
|
|
276
283
|
resolvedContent: ReactNode;
|
|
284
|
+
fullTitle: string | null;
|
|
277
285
|
itemPadding: number;
|
|
278
286
|
contentRef?: ((node: HTMLSpanElement | null) => void) | null;
|
|
279
287
|
ref?: ((node: HTMLAnchorElement | null) => void) | null;
|
|
@@ -283,15 +291,19 @@ function PortableClerkTOCItem({
|
|
|
283
291
|
ref={ref}
|
|
284
292
|
href={item.url}
|
|
285
293
|
data-clerk-item=""
|
|
294
|
+
title={fullTitle ?? undefined}
|
|
286
295
|
style={{
|
|
287
296
|
paddingInlineStart: itemPadding,
|
|
288
297
|
}}
|
|
289
298
|
className={cn(
|
|
290
|
-
'prose group relative py-1.5 text-sm transition-colors
|
|
299
|
+
'prose group relative py-1.5 text-sm transition-colors first:pt-0 last:pb-0 hover:text-fd-accent-foreground',
|
|
291
300
|
isActive ? themeIconColor : 'text-fd-muted-foreground',
|
|
292
301
|
)}
|
|
293
302
|
>
|
|
294
|
-
<span
|
|
303
|
+
<span
|
|
304
|
+
ref={contentRef}
|
|
305
|
+
className="relative z-10 block overflow-hidden text-ellipsis whitespace-nowrap"
|
|
306
|
+
>
|
|
295
307
|
{resolvedContent}
|
|
296
308
|
</span>
|
|
297
309
|
</Primitive.TOCItem>
|
|
@@ -453,22 +465,34 @@ function getItemOffset(depth: number): number {
|
|
|
453
465
|
}
|
|
454
466
|
|
|
455
467
|
function getLineOffset(depth: number): number {
|
|
456
|
-
|
|
468
|
+
const group = getDepthGroup(depth);
|
|
469
|
+
return CLERK_DEPTH_GROUP_LINE_OFFSETS[group];
|
|
457
470
|
}
|
|
458
471
|
|
|
459
472
|
function getVisualLinePosition(depth: number): number {
|
|
460
473
|
return getLineOffset(depth);
|
|
461
474
|
}
|
|
462
475
|
|
|
476
|
+
function getDepthGroup(depth: number): number {
|
|
477
|
+
if (depth <= 2) return 0;
|
|
478
|
+
if (depth === 3) return 1;
|
|
479
|
+
if (depth === 4) return 2;
|
|
480
|
+
return 3;
|
|
481
|
+
}
|
|
482
|
+
|
|
463
483
|
function resolveClerkItem(item: TOCItemType): ClerkItemMeta {
|
|
464
484
|
const isH3 = item.depth === 3;
|
|
465
485
|
const rawTitle = typeof item.title === 'string' ? item.title : '';
|
|
466
486
|
const { isStep, displayStep, content } = getStepInfoFromTitle(rawTitle);
|
|
467
487
|
let stepNumber: string | null = isH3 && isStep ? String(displayStep) : null;
|
|
468
488
|
let resolvedContent: ReactNode = item.title;
|
|
489
|
+
let fullTitle: string | null = rawTitle || null;
|
|
469
490
|
|
|
470
491
|
if (isH3 && isStep) {
|
|
471
492
|
resolvedContent = content ?? item.title;
|
|
493
|
+
if (typeof content === 'string') {
|
|
494
|
+
fullTitle = content;
|
|
495
|
+
}
|
|
472
496
|
}
|
|
473
497
|
|
|
474
498
|
if (isH3 && !stepNumber) {
|
|
@@ -480,20 +504,34 @@ function resolveClerkItem(item: TOCItemType): ClerkItemMeta {
|
|
|
480
504
|
const match = rawTitle.match(/^(\d+(?:\.\d+)*\.?)\s+(.+)$/);
|
|
481
505
|
if (match?.[2]) {
|
|
482
506
|
resolvedContent = match[2];
|
|
507
|
+
fullTitle = match[2];
|
|
483
508
|
}
|
|
484
509
|
}
|
|
485
510
|
}
|
|
486
511
|
}
|
|
487
512
|
|
|
513
|
+
if (typeof resolvedContent === 'string') {
|
|
514
|
+
fullTitle = resolvedContent;
|
|
515
|
+
resolvedContent = truncateClerkLabel(resolvedContent);
|
|
516
|
+
}
|
|
517
|
+
|
|
488
518
|
return {
|
|
489
519
|
item,
|
|
490
520
|
resolvedContent,
|
|
521
|
+
fullTitle,
|
|
491
522
|
stepNumber,
|
|
492
523
|
itemPadding: getItemOffset(item.depth),
|
|
493
524
|
lineOffset: getVisualLinePosition(item.depth),
|
|
494
525
|
};
|
|
495
526
|
}
|
|
496
527
|
|
|
528
|
+
function truncateClerkLabel(value: string): string {
|
|
529
|
+
const normalized = value.trim();
|
|
530
|
+
if (normalized.length <= CLERK_MAX_LABEL_LENGTH) return normalized;
|
|
531
|
+
|
|
532
|
+
return `${normalized.slice(0, CLERK_MAX_LABEL_LENGTH).trimEnd()}...`;
|
|
533
|
+
}
|
|
534
|
+
|
|
497
535
|
function buildOutlinePath(items: ClerkItemMeasure[]): string {
|
|
498
536
|
if (items.length === 0) return '';
|
|
499
537
|
|
package/src/styles/fuma.css
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
@apply top-20!;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
@media (min-width: 1280px) {
|
|
7
|
+
/* 给桌面端固定 TOC 留一点底部呼吸感,避免视觉上直接压到 footer */
|
|
8
|
+
#nd-toc {
|
|
9
|
+
bottom: 1.25rem !important;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* 文档主区域补一小段底部缓冲,减少 footer 刚进入视口就与 TOC 相撞 */
|
|
13
|
+
#nd-page {
|
|
14
|
+
padding-bottom: 5rem !important;
|
|
15
|
+
min-height: calc(100dvh - 5rem) !important;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
6
19
|
/* 移动端文章目录下拉框的边距,建议移动端不要目录导航 96px */
|
|
7
20
|
#nd-tocnav {
|
|
8
21
|
@apply top-24!;
|
|
@@ -35,7 +48,7 @@ button[aria-label="Open Search"][data-search=""] {
|
|
|
35
48
|
|
|
36
49
|
/* Custome Fuma Steps */
|
|
37
50
|
.fd-step::before {
|
|
38
|
-
@apply size-5 -
|
|
51
|
+
@apply size-5 -inset-s-2.5 rounded-full;
|
|
39
52
|
background-color: #000;
|
|
40
53
|
color: #fff;
|
|
41
54
|
font-size: 0.75rem;
|
|
@@ -128,4 +141,4 @@ button[aria-label="Open Search"][data-search=""] {
|
|
|
128
141
|
[data-rmiz-modal-img] {
|
|
129
142
|
transition-duration: 0.01ms !important;
|
|
130
143
|
}
|
|
131
|
-
}
|
|
144
|
+
}
|