@windrun-huaiin/third-ui 16.0.1 → 20.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.
Files changed (30) hide show
  1. package/dist/clerk/fingerprint/fingerprint-provider.js +58 -49
  2. package/dist/clerk/fingerprint/fingerprint-provider.mjs +58 -49
  3. package/dist/main/alert-dialog/ads-alert-dialog.d.ts +15 -0
  4. package/dist/main/alert-dialog/ads-alert-dialog.js +24 -0
  5. package/dist/main/alert-dialog/ads-alert-dialog.mjs +22 -0
  6. package/dist/main/alert-dialog/confirm-dialog.d.ts +15 -0
  7. package/dist/main/alert-dialog/confirm-dialog.js +40 -0
  8. package/dist/main/alert-dialog/confirm-dialog.mjs +38 -0
  9. package/dist/main/alert-dialog/dialog-styles.d.ts +14 -0
  10. package/dist/main/alert-dialog/dialog-styles.js +35 -0
  11. package/dist/main/alert-dialog/dialog-styles.mjs +20 -0
  12. package/dist/main/alert-dialog/high-priority-confirm-dialog.d.ts +12 -0
  13. package/dist/main/alert-dialog/high-priority-confirm-dialog.js +23 -0
  14. package/dist/main/alert-dialog/high-priority-confirm-dialog.mjs +21 -0
  15. package/dist/main/alert-dialog/index.d.ts +4 -0
  16. package/dist/main/alert-dialog/info-dialog.d.ts +13 -0
  17. package/dist/main/alert-dialog/info-dialog.js +50 -0
  18. package/dist/main/alert-dialog/info-dialog.mjs +48 -0
  19. package/dist/main/index.d.ts +1 -1
  20. package/dist/main/index.js +7 -1
  21. package/dist/main/index.mjs +4 -1
  22. package/package.json +4 -4
  23. package/src/clerk/fingerprint/fingerprint-provider.tsx +155 -62
  24. package/src/main/{ads-alert-dialog.tsx → alert-dialog/ads-alert-dialog.tsx} +46 -29
  25. package/src/main/alert-dialog/confirm-dialog.tsx +131 -0
  26. package/src/main/alert-dialog/dialog-styles.ts +73 -0
  27. package/src/main/alert-dialog/high-priority-confirm-dialog.tsx +94 -0
  28. package/src/main/alert-dialog/index.ts +7 -0
  29. package/src/main/alert-dialog/info-dialog.tsx +139 -0
  30. package/src/main/index.ts +1 -1
@@ -15,6 +15,7 @@ import {
15
15
  } from '@windrun-huaiin/base-ui/icons';
16
16
  import { themeButtonGradientClass, themeButtonGradientHoverClass, themeIconColor } from '@windrun-huaiin/base-ui/lib';
17
17
  import { cn } from '@windrun-huaiin/lib/utils';
18
+ import { useMessages } from 'next-intl';
18
19
  import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
19
20
  import type { FingerprintContextType, FingerprintProviderProps } from './types';
20
21
  import { useFingerprint } from './use-fingerprint';
@@ -28,6 +29,83 @@ import { FINGERPRINT_SOURCE_REFER } from './fingerprint-shared';
28
29
 
29
30
  const FingerprintContext = createContext<FingerprintContextType | undefined>(undefined);
30
31
 
32
+ type FingerprintStatusTranslations = {
33
+ panel: {
34
+ toggleAriaLabel: string;
35
+ title: string;
36
+ testModeLabel: string;
37
+ closeAriaLabel: string;
38
+ };
39
+ sections: {
40
+ user: string;
41
+ creditsInfo: string;
42
+ subscription: string;
43
+ concurrentBaseInfo: string;
44
+ };
45
+ labels: {
46
+ userId: string;
47
+ nickName: string;
48
+ fingerprintId: string;
49
+ clerkUserId: string;
50
+ email: string;
51
+ stripeCusId: string;
52
+ createdAt: string;
53
+ plan: string;
54
+ period: string;
55
+ allocated: string;
56
+ subId: string;
57
+ orderId: string;
58
+ priceId: string;
59
+ realBrowser: string;
60
+ testOverride: string;
61
+ };
62
+ creditBuckets: {
63
+ paid: string;
64
+ oneTimePaid: string;
65
+ free: string;
66
+ };
67
+ placeholders: {
68
+ subscriptionStatusNever: string;
69
+ subscriptionPeriodUnavailable: string;
70
+ noCreditsYet: string;
71
+ noRecords: string;
72
+ noTestExecutedYet: string;
73
+ none: string;
74
+ unknownError: string;
75
+ };
76
+ status: {
77
+ pending: string;
78
+ idle: string;
79
+ };
80
+ actions: {
81
+ frontendPreventionTest: string;
82
+ backendIdempotencyTest: string;
83
+ generateNewTestFingerprintAriaLabel: string;
84
+ dismissErrorAriaLabel: string;
85
+ };
86
+ messages: {
87
+ testFingerprintNotReady: string;
88
+ runningFrontendPreventionTest: string;
89
+ frontendPreventionTestFinished: string;
90
+ frontendPreventionTestFailed: string;
91
+ failedToInitializeAnonymousUser: string;
92
+ runningBackendIdempotencyTest: string;
93
+ backendIdempotencyTestDone: string;
94
+ backendIdempotencyTestFailed: string;
95
+ generatedTestFingerprintOverride: string;
96
+ createdCount: string;
97
+ reusedCount: string;
98
+ failedCount: string;
99
+ createdUserIds: string;
100
+ failedStatuses: string;
101
+ };
102
+ };
103
+
104
+ function useFingerprintStatusTranslations(): FingerprintStatusTranslations {
105
+ const messages = useMessages() as Record<string, unknown>;
106
+ return messages.fingerprint as FingerprintStatusTranslations;
107
+ }
108
+
31
109
  /**
32
110
  * Fingerprint Provider Component
33
111
  * 为应用提供fingerprint和匿名用户管理功能
@@ -86,6 +164,7 @@ export function withFingerprint<P extends object>(
86
164
  * 组件:显示用户状态和积分信息(用于调试)
87
165
  */
88
166
  export function FingerprintStatus() {
167
+ const translations = useFingerprintStatusTranslations();
89
168
  const {
90
169
  fingerprintId,
91
170
  xUser,
@@ -136,7 +215,7 @@ export function FingerprintStatus() {
136
215
  return [
137
216
  {
138
217
  key: 'paid',
139
- label: 'Paid',
218
+ label: translations.creditBuckets.paid,
140
219
  icon: <Settings2Icon className="size-4 text-green-500 dark:text-green-300" />,
141
220
  balance: xCredit.balancePaid,
142
221
  total: xCredit.totalPaidLimit,
@@ -145,7 +224,7 @@ export function FingerprintStatus() {
145
224
  },
146
225
  {
147
226
  key: 'oneTimePaid',
148
- label: 'OneTimePaid',
227
+ label: translations.creditBuckets.oneTimePaid,
149
228
  icon: <CoinsIcon className="size-4 text-amber-500 dark:text-amber-300" />,
150
229
  balance: xCredit.balanceOneTimePaid,
151
230
  total: xCredit.totalOneTimePaidLimit,
@@ -154,7 +233,7 @@ export function FingerprintStatus() {
154
233
  },
155
234
  {
156
235
  key: 'free',
157
- label: 'Free',
236
+ label: translations.creditBuckets.free,
158
237
  icon: <GiftIcon className="size-4 text-purple-500 dark:text-purple-300" />,
159
238
  balance: xCredit.balanceFree,
160
239
  total: xCredit.totalFreeLimit,
@@ -162,15 +241,15 @@ export function FingerprintStatus() {
162
241
  end: xCredit.freeEnd,
163
242
  },
164
243
  ];
165
- }, [xCredit]);
244
+ }, [translations.creditBuckets.free, translations.creditBuckets.oneTimePaid, translations.creditBuckets.paid, xCredit]);
166
245
 
167
246
  const subscriptionStatus = useMemo(() => {
168
247
  if (!xSubscription) {
169
248
  return {
170
- status: 'Never',
249
+ status: translations.placeholders.subscriptionStatusNever,
171
250
  priceName: '--',
172
251
  creditsAllocated: '--',
173
- period: 'Unavailable',
252
+ period: translations.placeholders.subscriptionPeriodUnavailable,
174
253
  };
175
254
  }
176
255
  return {
@@ -179,9 +258,9 @@ export function FingerprintStatus() {
179
258
  creditsAllocated: typeof xSubscription.creditsAllocated === 'number'
180
259
  ? formatNumber(xSubscription.creditsAllocated)
181
260
  : '--',
182
- period: formatRangeText(xSubscription.subPeriodStart, xSubscription.subPeriodEnd),
261
+ period: formatRangeText(xSubscription.subPeriodStart, xSubscription.subPeriodEnd, translations),
183
262
  };
184
- }, [xSubscription]);
263
+ }, [translations.placeholders.subscriptionPeriodUnavailable, translations.placeholders.subscriptionStatusNever, xSubscription]);
185
264
 
186
265
  const userStatus = xUser?.status || '--';
187
266
  const totalCredits = formatNumber(xCredit?.totalBalance);
@@ -195,13 +274,13 @@ export function FingerprintStatus() {
195
274
  const runContextParallelInitTest = async () => {
196
275
  const debugFingerprintId = activeDebugFingerprintId ?? getOrCreateDebugFingerprintOverride();
197
276
  if (!debugFingerprintId) {
198
- setTestResult('Test fingerprint override is not ready yet.');
277
+ setTestResult(translations.messages.testFingerprintNotReady);
199
278
  return;
200
279
  }
201
280
 
202
281
  setActiveDebugFingerprintId(debugFingerprintId);
203
282
  setIsRunningTest(true);
204
- setTestResult(`Running Frontend Prevention Test with fingerprint: ${debugFingerprintId}`);
283
+ setTestResult(tpl(translations.messages.runningFrontendPreventionTest, { fingerprintId: debugFingerprintId }));
205
284
 
206
285
  try {
207
286
  await Promise.all([
@@ -209,9 +288,9 @@ export function FingerprintStatus() {
209
288
  initializeDebugAnonymousUser(debugFingerprintId),
210
289
  initializeDebugAnonymousUser(debugFingerprintId),
211
290
  ]);
212
- setTestResult(`Frontend Prevention Test finished. Active test fingerprint: ${debugFingerprintId}`);
291
+ setTestResult(tpl(translations.messages.frontendPreventionTestFinished, { fingerprintId: debugFingerprintId }));
213
292
  } catch (testError) {
214
- setTestResult(`Frontend Prevention Test failed: ${formatErrorMessage(testError)}`);
293
+ setTestResult(tpl(translations.messages.frontendPreventionTestFailed, { error: formatErrorMessage(testError, translations) }));
215
294
  } finally {
216
295
  setIsRunningTest(false);
217
296
  }
@@ -239,7 +318,7 @@ export function FingerprintStatus() {
239
318
 
240
319
  if (!response.ok) {
241
320
  const errorData = await response.json().catch(() => ({}));
242
- throw new Error(errorData.error || 'Failed to initialize anonymous user');
321
+ throw new Error(errorData.error || translations.messages.failedToInitializeAnonymousUser);
243
322
  }
244
323
 
245
324
  await response.json().catch(() => ({}));
@@ -251,13 +330,13 @@ export function FingerprintStatus() {
251
330
  const runRawParallelPostTest = async () => {
252
331
  const normalizedFingerprintId = activeDebugFingerprintId ?? getOrCreateDebugFingerprintOverride();
253
332
  if (!normalizedFingerprintId) {
254
- setTestResult('Test fingerprint override is not ready yet.');
333
+ setTestResult(translations.messages.testFingerprintNotReady);
255
334
  return;
256
335
  }
257
336
 
258
337
  setActiveDebugFingerprintId(normalizedFingerprintId);
259
338
  setIsRunningTest(true);
260
- setTestResult(`Running Backend Idempotency Test with fingerprint: ${normalizedFingerprintId}`);
339
+ setTestResult(tpl(translations.messages.runningBackendIdempotencyTest, { fingerprintId: normalizedFingerprintId }));
261
340
 
262
341
  try {
263
342
  const fingerprintHeaders = await createFingerprintHeaders();
@@ -297,16 +376,16 @@ export function FingerprintStatus() {
297
376
 
298
377
  setTestResult(
299
378
  [
300
- `Backend Idempotency Test done.`,
301
- `created=${createdUserIds.length}`,
302
- `reused=${reusedUserIds.length}`,
303
- `failed=${failedStatuses.length}`,
304
- `createdUserIds=[${createdUserIds.join(', ')}]`,
305
- failedStatuses.length > 0 ? `failedStatuses=[${failedStatuses.join(', ')}]` : null,
379
+ translations.messages.backendIdempotencyTestDone,
380
+ tpl(translations.messages.createdCount, { count: createdUserIds.length }),
381
+ tpl(translations.messages.reusedCount, { count: reusedUserIds.length }),
382
+ tpl(translations.messages.failedCount, { count: failedStatuses.length }),
383
+ tpl(translations.messages.createdUserIds, { value: createdUserIds.join(', ') }),
384
+ failedStatuses.length > 0 ? tpl(translations.messages.failedStatuses, { value: failedStatuses.join(', ') }) : null,
306
385
  ].filter(Boolean).join('\n')
307
386
  );
308
387
  } catch (testError) {
309
- setTestResult(`Backend Idempotency Test failed: ${formatErrorMessage(testError)}`);
388
+ setTestResult(tpl(translations.messages.backendIdempotencyTestFailed, { error: formatErrorMessage(testError, translations) }));
310
389
  } finally {
311
390
  setIsRunningTest(false);
312
391
  }
@@ -315,7 +394,7 @@ export function FingerprintStatus() {
315
394
  const regenerateTestFingerprint = () => {
316
395
  const nextFingerprintId = regenerateDebugFingerprintOverride();
317
396
  setActiveDebugFingerprintId(nextFingerprintId);
318
- setTestResult(`Generated test fingerprint override: ${nextFingerprintId}`);
397
+ setTestResult(tpl(translations.messages.generatedTestFingerprintOverride, { fingerprintId: nextFingerprintId }));
319
398
  };
320
399
 
321
400
  return (
@@ -325,7 +404,7 @@ export function FingerprintStatus() {
325
404
  <button
326
405
  onClick={handleToggle}
327
406
  type="button"
328
- aria-label="Fingerprint debug panel"
407
+ aria-label={translations.panel.toggleAriaLabel}
329
408
  className={cn(
330
409
  'fixed left-2 top-2 md:left-2 md:top-3 z-10000 inline-flex size-8 md:size-11 items-center justify-center rounded-full',
331
410
  themeButtonGradientClass,
@@ -354,7 +433,7 @@ export function FingerprintStatus() {
354
433
  <div className="flex items-start justify-between gap-3">
355
434
  <div className={cn("flex items-center gap-2 text-base font-bold tracking-wider", themeIconColor)}>
356
435
  <ShieldUserIcon className="size-4" />
357
- Fingerprint Debug Panel
436
+ {translations.panel.title}
358
437
  </div>
359
438
  <div className="flex items-center gap-2">
360
439
  <button
@@ -368,7 +447,7 @@ export function FingerprintStatus() {
368
447
  )}
369
448
  aria-pressed={panelMode === 'test'}
370
449
  >
371
- <span>Concurrent Test</span>
450
+ <span>{translations.panel.testModeLabel}</span>
372
451
  <span
373
452
  className={cn(
374
453
  'relative inline-flex h-5 w-9 items-center rounded-full transition-colors',
@@ -388,7 +467,7 @@ export function FingerprintStatus() {
388
467
  </button>
389
468
  <button
390
469
  type="button"
391
- aria-label="Close fingerprint panel"
470
+ aria-label={translations.panel.closeAriaLabel}
392
471
  className="rounded-full p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-700 dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-white"
393
472
  onClick={() => setIsOpen(false)}
394
473
  >
@@ -403,23 +482,23 @@ export function FingerprintStatus() {
403
482
  <>
404
483
  <PanelSection
405
484
  icon={<FingerprintIcon className="size-4" />}
406
- title="User"
407
- rightInfo={<StatusTag value={userStatus} />}
485
+ title={translations.sections.user}
486
+ rightInfo={<StatusTag value={userStatus} translations={translations} />}
408
487
  items={[
409
- { label: 'UserID', value: <CopyableText text={xUser?.userId || ''} /> },
410
- { label: 'NickName', value: <CopyableText text={xUser?.userName || ''} /> },
411
- { label: 'FingerprintID', value: <CopyableText text={xUser?.fingerprintId || fingerprintId || ''} /> },
412
- { label: 'ClerkUserID', value: <CopyableText text={xUser?.clerkUserId || ''} /> },
413
- { label: 'Email', value: <CopyableText text={xUser?.email || ''} /> },
414
- { label: 'StripeCusID', value: <CopyableText text={xUser?.stripeCusId || ''} /> },
415
- { label: 'CreatedAt', value: xUser?.createdAt || '--' },
488
+ { label: translations.labels.userId, value: <CopyableText text={xUser?.userId || ''} /> },
489
+ { label: translations.labels.nickName, value: <CopyableText text={xUser?.userName || ''} /> },
490
+ { label: translations.labels.fingerprintId, value: <CopyableText text={xUser?.fingerprintId || fingerprintId || ''} /> },
491
+ { label: translations.labels.clerkUserId, value: <CopyableText text={xUser?.clerkUserId || ''} /> },
492
+ { label: translations.labels.email, value: <CopyableText text={xUser?.email || ''} /> },
493
+ { label: translations.labels.stripeCusId, value: <CopyableText text={xUser?.stripeCusId || ''} /> },
494
+ { label: translations.labels.createdAt, value: xUser?.createdAt || '--' },
416
495
  ]}
417
496
  />
418
497
 
419
498
  <div className="space-y-2 rounded-xl border border-slate-200/70 bg-white/80 p-4 shadow-sm dark:border-white/12 dark:bg-slate-900/50">
420
499
  <PanelHeader
421
500
  icon={<GemIcon className="size-4" />}
422
- title="Credits Info"
501
+ title={translations.sections.creditsInfo}
423
502
  rightInfo={<span className={cn("font-semibold", themeIconColor)}>{totalCredits}</span>}
424
503
  />
425
504
  <div className="space-y-3">
@@ -444,29 +523,29 @@ export function FingerprintStatus() {
444
523
  />
445
524
  </div>
446
525
  <div className="mt-2 flex items-center justify-between text-[11px] text-slate-500 dark:text-slate-400">
447
- <span>{formatRangeText(bucket.start, bucket.end)}</span>
526
+ <span>{formatRangeText(bucket.start, bucket.end, translations)}</span>
448
527
  <span>{percent}%</span>
449
528
  </div>
450
529
  </div>
451
530
  );
452
531
  })
453
532
  ) : (
454
- <EmptyPlaceholder label="No Credits Yet" icon={<DatabaseZapIcon className="size-4" />} />
533
+ <EmptyPlaceholder label={translations.placeholders.noCreditsYet} icon={<DatabaseZapIcon className="size-4" />} />
455
534
  )}
456
535
  </div>
457
536
  </div>
458
537
 
459
538
  <PanelSection
460
539
  icon={<BellIcon className="size-4" />}
461
- title="Subscription"
462
- rightInfo={<StatusTag value={subStatus} />}
540
+ title={translations.sections.subscription}
541
+ rightInfo={<StatusTag value={subStatus} translations={translations} />}
463
542
  items={[
464
- { label: 'Plan', value: subscriptionStatus.priceName },
465
- { label: 'Period', value: subscriptionStatus.period },
466
- { label: 'Allocated', value: subscriptionStatus.creditsAllocated },
467
- { label: 'SubID', value: <CopyableText text={xSubscription?.paySubscriptionId || ''} /> },
468
- { label: 'OrderID', value: <CopyableText text={xSubscription?.orderId || ''} /> },
469
- { label: 'PriceID', value: <CopyableText text={xSubscription?.priceId || ''} /> },
543
+ { label: translations.labels.plan, value: subscriptionStatus.priceName },
544
+ { label: translations.labels.period, value: subscriptionStatus.period },
545
+ { label: translations.labels.allocated, value: subscriptionStatus.creditsAllocated },
546
+ { label: translations.labels.subId, value: <CopyableText text={xSubscription?.paySubscriptionId || ''} /> },
547
+ { label: translations.labels.orderId, value: <CopyableText text={xSubscription?.orderId || ''} /> },
548
+ { label: translations.labels.priceId, value: <CopyableText text={xSubscription?.priceId || ''} /> },
470
549
  ]}
471
550
  />
472
551
  </>
@@ -474,17 +553,17 @@ export function FingerprintStatus() {
474
553
  <div className="space-y-3 rounded-xl border border-slate-200/70 bg-white/85 p-4 shadow-sm dark:border-white/12 dark:bg-slate-900/45">
475
554
  <PanelHeader
476
555
  icon={<DatabaseZapIcon className="size-4" />}
477
- title="Concurrent Base Info"
478
- rightInfo={<StatusTag value={isRunningTest ? 'pending' : 'idle'} />}
556
+ title={translations.sections.concurrentBaseInfo}
557
+ rightInfo={<StatusTag value={isRunningTest ? translations.status.pending : translations.status.idle} translations={translations} />}
479
558
  />
480
559
 
481
560
  <div className="space-y-2 text-xs text-slate-500 dark:text-slate-300">
482
561
  <div className="flex items-center justify-between gap-3">
483
- <span className="text-slate-400 dark:text-slate-500">Real Browser</span>
562
+ <span className="text-slate-400 dark:text-slate-500">{translations.labels.realBrowser}</span>
484
563
  <CopyableText text={fingerprintId || ''} />
485
564
  </div>
486
565
  <div className="space-y-1">
487
- <span className="text-slate-400 dark:text-slate-500">Test Override</span>
566
+ <span className="text-slate-400 dark:text-slate-500">{translations.labels.testOverride}</span>
488
567
  <div className="flex items-center gap-2 py-1">
489
568
  <div className="min-w-0 flex-1 rounded-lg border border-slate-200 bg-white px-3 py-2 font-mono text-[0.5rem] sm:text-[0.625rem] md:text-xs leading-tight text-slate-700 dark:border-white/10 dark:bg-slate-950 dark:text-slate-100">
490
569
  <CopyableText text={activeDebugFingerprintId || ''} />
@@ -493,7 +572,7 @@ export function FingerprintStatus() {
493
572
  type="button"
494
573
  disabled={isRunningTest}
495
574
  onClick={regenerateTestFingerprint}
496
- aria-label="Generate new test fingerprint"
575
+ aria-label={translations.actions.generateNewTestFingerprintAriaLabel}
497
576
  className="inline-flex size-9 items-center justify-center rounded-lg border border-slate-200 bg-slate-50 text-slate-700 transition hover:border-slate-300 hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/10 dark:bg-slate-950 dark:text-slate-100 dark:hover:bg-slate-900"
498
577
  >
499
578
  <RefreshCcwIcon className="size-4" />
@@ -512,7 +591,7 @@ export function FingerprintStatus() {
512
591
  themedGhostButtonClass
513
592
  )}
514
593
  >
515
- Frontend Prevention Test
594
+ {translations.actions.frontendPreventionTest}
516
595
  </button>
517
596
  <button
518
597
  type="button"
@@ -525,13 +604,13 @@ export function FingerprintStatus() {
525
604
  themeButtonGradientHoverClass
526
605
  )}
527
606
  >
528
- Backend Idempotency Test
607
+ {translations.actions.backendIdempotencyTest}
529
608
  </button>
530
609
  </div>
531
610
 
532
611
  <div className="rounded-lg border border-dashed border-slate-200 bg-slate-50/80 p-3 dark:border-white/10 dark:bg-slate-950/50">
533
612
  <pre className="overflow-x-auto whitespace-pre-wrap break-all font-mono text-[11px] leading-5 text-slate-600 dark:text-slate-300">
534
- {testResult || 'No test executed yet.'}
613
+ {testResult || translations.placeholders.noTestExecutedYet}
535
614
  </pre>
536
615
  </div>
537
616
  </div>
@@ -545,7 +624,7 @@ export function FingerprintStatus() {
545
624
  </div>
546
625
  <button
547
626
  type="button"
548
- aria-label="Dismiss error"
627
+ aria-label={translations.actions.dismissErrorAriaLabel}
549
628
  onClick={clearError}
550
629
  className="shrink-0 rounded-full p-1 text-amber-500 transition hover:bg-amber-100 hover:text-amber-700 dark:text-amber-200 dark:hover:bg-amber-500/10 dark:hover:text-amber-100"
551
630
  >
@@ -632,12 +711,16 @@ function computeProgress(balance: number | null | undefined, total: number | nul
632
711
  return Math.min(Math.max(ratio, 0), 1);
633
712
  }
634
713
 
635
- function formatRangeText(start: string | null | undefined, end: string | null | undefined) {
714
+ function formatRangeText(
715
+ start: string | null | undefined,
716
+ end: string | null | undefined,
717
+ translations: FingerprintStatusTranslations
718
+ ) {
636
719
  const safeStart = start && start.trim() ? start : '';
637
720
  const safeEnd = end && end.trim() ? end : '';
638
721
 
639
722
  if (!safeStart && !safeEnd) {
640
- return 'No records';
723
+ return translations.placeholders.noRecords;
641
724
  }
642
725
 
643
726
  if (!safeStart) {
@@ -651,8 +734,14 @@ function formatRangeText(start: string | null | undefined, end: string | null |
651
734
  return `${safeStart} - ${safeEnd}`;
652
735
  }
653
736
 
654
- function StatusTag({ value }: { value: string | undefined | null }) {
655
- if (!value) return <span className="text-slate-400">None</span>;
737
+ function StatusTag({
738
+ value,
739
+ translations,
740
+ }: {
741
+ value: string | undefined | null;
742
+ translations: FingerprintStatusTranslations;
743
+ }) {
744
+ if (!value) return <span className="text-slate-400">{translations.placeholders.none}</span>;
656
745
 
657
746
  const normalized = value.toLowerCase();
658
747
 
@@ -689,7 +778,7 @@ function StatusTag({ value }: { value: string | undefined | null }) {
689
778
  );
690
779
  }
691
780
 
692
- function formatErrorMessage(error: unknown) {
781
+ function formatErrorMessage(error: unknown, translations: FingerprintStatusTranslations) {
693
782
  if (error instanceof Error) {
694
783
  return error.message;
695
784
  }
@@ -698,5 +787,9 @@ function formatErrorMessage(error: unknown) {
698
787
  return error;
699
788
  }
700
789
 
701
- return 'Unknown error';
790
+ return translations.placeholders.unknownError;
791
+ }
792
+
793
+ function tpl(template: string, values: Record<string, string | number>) {
794
+ return template.replace(/\{(\w+)\}/g, (_, key: string) => String(values[key] ?? ''));
702
795
  }
@@ -1,15 +1,28 @@
1
1
  'use client';
2
2
 
3
- import React, { useState } from "react";
4
- import Image from "next/image";
5
- import { ImageOffIcon, InfoIcon, XIcon } from "@windrun-huaiin/base-ui/icons";
3
+ import React, { useState } from 'react';
4
+ import Image from 'next/image';
5
+ import { BellIcon, ImageOffIcon, XIcon } from '@windrun-huaiin/base-ui/icons';
6
6
  import {
7
7
  AlertDialog,
8
+ AlertDialogAction,
8
9
  AlertDialogContent,
9
- AlertDialogTitle,
10
10
  AlertDialogDescription,
11
- AlertDialogAction,
12
- } from "@windrun-huaiin/base-ui/ui";
11
+ AlertDialogTitle,
12
+ } from '@windrun-huaiin/base-ui/ui';
13
+ import { cn } from '@windrun-huaiin/lib/utils';
14
+ import {
15
+ closeButtonClass,
16
+ dialogContentClass,
17
+ dialogDescriptionClass,
18
+ dialogFooterClass,
19
+ dialogHeaderClass,
20
+ dialogThemedOverlayClass,
21
+ dialogTitleClass,
22
+ primaryButtonClass,
23
+ secondaryButtonClass,
24
+ subtlePrimaryButtonClass,
25
+ } from './dialog-styles';
13
26
 
14
27
  interface AdsAlertDialogProps {
15
28
  open: boolean;
@@ -37,49 +50,52 @@ export function AdsAlertDialog({
37
50
  onConfirm,
38
51
  }: AdsAlertDialogProps) {
39
52
  const [imgError, setImgError] = useState(false);
53
+ const handleClose = () => onOpenChange(false);
40
54
 
41
55
  return (
42
56
  <AlertDialog open={open} onOpenChange={onOpenChange}>
43
57
  <AlertDialogContent
44
- className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white dark:bg-neutral-900 rounded-2xl shadow-2xl border border-neutral-200 dark:border-neutral-700 max-w-md w-full min-w-[320px] p-4 flex flex-col items-stretch"
58
+ className={cn(dialogContentClass, 'max-w-md p-4')}
59
+ overlayClassName={dialogThemedOverlayClass}
60
+ onOverlayClick={handleClose}
45
61
  >
46
- {/* Header: left icon + title, right X close */}
47
- <div className="flex flex-row items-center justify-between mb-2">
62
+ <div className={dialogHeaderClass}>
48
63
  <AlertDialogTitle asChild>
49
- <div className="flex flex-row items-center gap-1 min-w-0 text-xl font-semibold">
50
- <InfoIcon className="w-5 h-5" />
64
+ <div className={dialogTitleClass}>
65
+ <span className="inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-neutral-100 text-neutral-600 ring-1 ring-neutral-200 dark:bg-neutral-900 dark:text-neutral-300 dark:ring-neutral-800">
66
+ <BellIcon className="size-5" />
67
+ </span>
51
68
  <span className="truncate">{title}</span>
52
69
  </div>
53
70
  </AlertDialogTitle>
54
71
  <button
55
- className="text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200 text-xl ml-4"
56
- onClick={() => onOpenChange(false)}
72
+ type="button"
73
+ className={closeButtonClass}
74
+ onClick={handleClose}
57
75
  aria-label="Close"
58
- tabIndex={0}
59
76
  >
60
- <XIcon className="w-5 h-5" />
77
+ <XIcon className="size-4" />
61
78
  </button>
62
79
  </div>
63
-
64
- {/* description area */}
65
- <AlertDialogDescription className="text-base font-medium text-neutral-800 dark:text-neutral-100 mb-2">
80
+
81
+ <AlertDialogDescription className={cn(dialogDescriptionClass, 'mb-3 text-base text-neutral-800 dark:text-neutral-100')}>
66
82
  {description}
67
83
  </AlertDialogDescription>
68
- {/* image area (optional) */}
84
+
69
85
  {imgSrc && (
70
- <div className="w-full max-w-[400px] h-[220px] relative flex items-center justify-center mb-2">
86
+ <div className="relative mb-2 flex h-[220px] w-full max-w-[400px] items-center justify-center overflow-hidden rounded-xl border border-neutral-200 bg-neutral-50 dark:border-neutral-800 dark:bg-neutral-900">
71
87
  {imgError ? (
72
- <div className="absolute inset-0 flex flex-col items-center justify-center bg-gray-100 dark:bg-neutral-800 border border-dashed border-neutral-300 dark:border-neutral-700 rounded-lg text-neutral-400 text-sm">
73
- <ImageOffIcon className="w-12 h-12 mb-2" />
88
+ <div className="absolute inset-0 flex flex-col items-center justify-center border border-dashed border-neutral-300 text-sm text-neutral-400 dark:border-neutral-700">
89
+ <ImageOffIcon className="mb-2 size-12" />
74
90
  <span>Image loading failed</span>
75
91
  </div>
76
92
  ) : imgHref ? (
77
- <a href={imgHref} target="_blank" rel="noopener noreferrer" className="block w-full h-full">
93
+ <a href={imgHref} target="_blank" rel="noopener noreferrer" className="block h-full w-full">
78
94
  <Image
79
95
  src={imgSrc}
80
96
  alt="image"
81
97
  fill
82
- className="object-contain rounded-lg"
98
+ className="rounded-lg object-contain"
83
99
  priority={false}
84
100
  placeholder="empty"
85
101
  unoptimized
@@ -92,7 +108,7 @@ export function AdsAlertDialog({
92
108
  src={imgSrc}
93
109
  alt="image"
94
110
  fill
95
- className="object-contain rounded-lg"
111
+ className="rounded-lg object-contain"
96
112
  priority={false}
97
113
  placeholder="empty"
98
114
  unoptimized
@@ -102,16 +118,17 @@ export function AdsAlertDialog({
102
118
  )}
103
119
  </div>
104
120
  )}
105
- {/* button area (optional) */}
121
+
106
122
  {(cancelText || confirmText) && (
107
- <div className="flex justify-end gap-2 mt-2">
123
+ <div className={dialogFooterClass}>
108
124
  {cancelText && (
109
125
  <button
126
+ type="button"
110
127
  onClick={() => {
111
128
  onOpenChange(false);
112
129
  onCancel?.();
113
130
  }}
114
- className="px-6 py-2 rounded-lg border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-700 dark:text-neutral-200 font-semibold hover:bg-neutral-100 dark:hover:bg-neutral-700 transition"
131
+ className={secondaryButtonClass}
115
132
  >
116
133
  {cancelText}
117
134
  </button>
@@ -122,7 +139,7 @@ export function AdsAlertDialog({
122
139
  onOpenChange(false);
123
140
  onConfirm?.();
124
141
  }}
125
- className="px-6 py-2 rounded-lg bg-purple-500 text-white font-semibold hover:bg-purple-600 transition"
142
+ className={confirmText && !cancelText ? subtlePrimaryButtonClass : primaryButtonClass}
126
143
  >
127
144
  {confirmText}
128
145
  </AlertDialogAction>