@windrun-huaiin/third-ui 29.0.1 → 29.0.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-provider.js +3 -5
- package/dist/clerk/fingerprint/fingerprint-provider.mjs +3 -5
- package/dist/main/alert-dialog/index.d.ts +1 -0
- package/dist/main/alert-dialog/index.js +2 -0
- package/dist/main/alert-dialog/index.mjs +1 -0
- package/dist/main/alert-dialog/undoable-confirm-dialog.d.ts +17 -0
- package/dist/main/alert-dialog/undoable-confirm-dialog.js +87 -0
- package/dist/main/alert-dialog/undoable-confirm-dialog.mjs +85 -0
- package/dist/main/buttons/index.d.ts +1 -0
- package/dist/main/buttons/index.js +2 -0
- package/dist/main/buttons/index.mjs +1 -0
- package/dist/main/buttons/x-switch-button.d.ts +22 -0
- package/dist/main/buttons/x-switch-button.js +63 -0
- package/dist/main/buttons/x-switch-button.mjs +42 -0
- package/package.json +2 -2
- package/src/clerk/fingerprint/fingerprint-provider.tsx +9 -28
- package/src/main/alert-dialog/index.ts +1 -1
- package/src/main/alert-dialog/undoable-confirm-dialog.tsx +215 -0
- package/src/main/buttons/index.ts +1 -0
- package/src/main/buttons/x-switch-button.tsx +160 -0
- package/src/styles/fuma.css +1 -0
|
@@ -10,6 +10,8 @@ var nextIntl = require('next-intl');
|
|
|
10
10
|
var React = require('react');
|
|
11
11
|
var useFingerprint = require('./use-fingerprint.js');
|
|
12
12
|
var ui = require('@windrun-huaiin/base-ui/ui');
|
|
13
|
+
require('next/link');
|
|
14
|
+
var xSwitchButton = require('../../main/buttons/x-switch-button.js');
|
|
13
15
|
var fingerprintClient = require('./fingerprint-client.js');
|
|
14
16
|
var fingerprintDebug = require('./fingerprint-debug.js');
|
|
15
17
|
var fingerprintShared = require('./fingerprint-shared.js');
|
|
@@ -253,11 +255,7 @@ function FingerprintStatus() {
|
|
|
253
255
|
setActiveDebugFingerprintId(nextFingerprintId);
|
|
254
256
|
setTestResult(tpl(translations.messages.generatedTestFingerprintOverride, { fingerprintId: nextFingerprintId }));
|
|
255
257
|
};
|
|
256
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [!isOpen && (jsxRuntime.jsx("button", { onClick: handleToggle, type: "button", "aria-label": translations.panel.toggleAriaLabel, className: utils.cn('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', lib.themeButtonGradientClass, lib.themeButtonGradientHoverClass, 'text-white rounded-full shadow-lg hover:shadow-xl transition-all duration-300'), children: jsxRuntime.jsx(icons.LightbulbIcon, { className: "size-6 text-white" }) })), isOpen && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { onClick: handleBackdropClick, className: "fixed inset-0 z-9998 bg-black/60 backdrop-blur-sm" }), jsxRuntime.jsxs("div", { ref: modalRef, className: utils.cn('fixed inset-3 z-9999 mx-auto w-[min(95vw,520px)] overflow-y-auto rounded-2xl border', 'border-slate-200/70 bg-white/95 p-4 shadow-2xl backdrop-blur-sm', 'font-sans text-sm text-slate-700 dark:border-white/12 dark:bg-slate-950/95 dark:text-slate-200', 'sm:inset-auto md:left-2 sm:top-1 md:right-auto sm:w-[min(520px,95vw)] sm:p-5'), children: [jsxRuntime.jsx("header", { className: "mb-4", children: jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-3", children: [jsxRuntime.jsxs("div", { className: utils.cn("flex items-center gap-2 text-base font-bold tracking-wider", lib.themeIconColor), children: [jsxRuntime.jsx(icons.ShieldUserIcon, { className: "size-4" }), translations.panel.title] }), jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [jsxRuntime.
|
|
257
|
-
? utils.cn('border-transparent text-white', lib.themeButtonGradientClass, lib.themeButtonGradientHoverClass)
|
|
258
|
-
: themedGhostButtonClass), "aria-pressed": panelMode === 'test', children: [jsxRuntime.jsx("span", { children: translations.panel.testModeLabel }), jsxRuntime.jsx("span", { className: utils.cn('relative inline-flex h-5 w-9 items-center rounded-full transition-colors', panelMode === 'test'
|
|
259
|
-
? 'bg-white/25'
|
|
260
|
-
: 'bg-slate-300 dark:bg-slate-700'), children: jsxRuntime.jsx("span", { className: utils.cn('inline-block size-4 rounded-full shadow-sm transition-transform', panelMode === 'test' ? 'bg-white' : 'bg-white dark:bg-slate-100', panelMode === 'test' ? 'translate-x-4' : 'translate-x-0.5') }) })] }), jsxRuntime.jsx("button", { type: "button", "aria-label": translations.panel.closeAriaLabel, 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", onClick: () => setIsOpen(false), children: jsxRuntime.jsx(icons.XIcon, { className: "size-4" }) })] })] }) }), jsxRuntime.jsxs("section", { className: "space-y-1", children: [panelMode === 'info' ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(PanelSection, { icon: jsxRuntime.jsx(icons.FingerprintIcon, { className: "size-4" }), title: translations.sections.user, rightInfo: jsxRuntime.jsx(StatusTag, { value: userStatus, translations: translations }), items: [
|
|
258
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [!isOpen && (jsxRuntime.jsx("button", { onClick: handleToggle, type: "button", "aria-label": translations.panel.toggleAriaLabel, className: utils.cn('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', lib.themeButtonGradientClass, lib.themeButtonGradientHoverClass, 'text-white rounded-full shadow-lg hover:shadow-xl transition-all duration-300'), children: jsxRuntime.jsx(icons.LightbulbIcon, { className: "size-6 text-white" }) })), isOpen && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { onClick: handleBackdropClick, className: "fixed inset-0 z-9998 bg-black/60 backdrop-blur-sm" }), jsxRuntime.jsxs("div", { ref: modalRef, className: utils.cn('fixed inset-3 z-9999 mx-auto w-[min(95vw,520px)] overflow-y-auto rounded-2xl border', 'border-slate-200/70 bg-white/95 p-4 shadow-2xl backdrop-blur-sm', 'font-sans text-sm text-slate-700 dark:border-white/12 dark:bg-slate-950/95 dark:text-slate-200', 'sm:inset-auto md:left-2 sm:top-1 md:right-auto sm:w-[min(520px,95vw)] sm:p-5'), children: [jsxRuntime.jsx("header", { className: "mb-4", children: jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-3", children: [jsxRuntime.jsxs("div", { className: utils.cn("flex items-center gap-2 text-base font-bold tracking-wider", lib.themeIconColor), children: [jsxRuntime.jsx(icons.ShieldUserIcon, { className: "size-4" }), translations.panel.title] }), jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [jsxRuntime.jsx(xSwitchButton.XSwitchButton, { type: "button", checked: panelMode === 'test', onCheckedChange: (nextChecked) => setPanelMode(nextChecked ? 'test' : 'info'), checkedLabel: translations.panel.testModeLabel, uncheckedLabel: translations.panel.testModeLabel, size: "compact", className: "border px-2 py-1 text-[11px] shadow-sm" }), jsxRuntime.jsx("button", { type: "button", "aria-label": translations.panel.closeAriaLabel, 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", onClick: () => setIsOpen(false), children: jsxRuntime.jsx(icons.XIcon, { className: "size-4" }) })] })] }) }), jsxRuntime.jsxs("section", { className: "space-y-1", children: [panelMode === 'info' ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(PanelSection, { icon: jsxRuntime.jsx(icons.FingerprintIcon, { className: "size-4" }), title: translations.sections.user, rightInfo: jsxRuntime.jsx(StatusTag, { value: userStatus, translations: translations }), items: [
|
|
261
259
|
{ label: translations.labels.userId, value: jsxRuntime.jsx(ui.CopyableText, { text: (xUser === null || xUser === void 0 ? void 0 : xUser.userId) || '' }) },
|
|
262
260
|
{ label: translations.labels.nickName, value: jsxRuntime.jsx(ui.CopyableText, { text: (xUser === null || xUser === void 0 ? void 0 : xUser.userName) || '' }) },
|
|
263
261
|
{ label: translations.labels.fingerprintId, value: jsxRuntime.jsx(ui.CopyableText, { text: (xUser === null || xUser === void 0 ? void 0 : xUser.fingerprintId) || fingerprintId || '' }) },
|
|
@@ -8,6 +8,8 @@ import { useMessages } from 'next-intl';
|
|
|
8
8
|
import { createContext, useContext, useState, useRef, useEffect, useMemo } from 'react';
|
|
9
9
|
import { useFingerprint } from './use-fingerprint.mjs';
|
|
10
10
|
import { CopyableText } from '@windrun-huaiin/base-ui/ui';
|
|
11
|
+
import 'next/link';
|
|
12
|
+
import { XSwitchButton } from '../../main/buttons/x-switch-button.mjs';
|
|
11
13
|
import { createFingerprintHeaders } from './fingerprint-client.mjs';
|
|
12
14
|
import { getOrCreateDebugFingerprintOverride, regenerateDebugFingerprintOverride } from './fingerprint-debug.mjs';
|
|
13
15
|
import { FINGERPRINT_SOURCE_REFER } from './fingerprint-shared.mjs';
|
|
@@ -251,11 +253,7 @@ function FingerprintStatus() {
|
|
|
251
253
|
setActiveDebugFingerprintId(nextFingerprintId);
|
|
252
254
|
setTestResult(tpl(translations.messages.generatedTestFingerprintOverride, { fingerprintId: nextFingerprintId }));
|
|
253
255
|
};
|
|
254
|
-
return (jsxs(Fragment, { children: [!isOpen && (jsx("button", { onClick: handleToggle, type: "button", "aria-label": translations.panel.toggleAriaLabel, className: cn('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', themeButtonGradientClass, themeButtonGradientHoverClass, 'text-white rounded-full shadow-lg hover:shadow-xl transition-all duration-300'), children: jsx(LightbulbIcon, { className: "size-6 text-white" }) })), isOpen && (jsxs(Fragment, { children: [jsx("div", { onClick: handleBackdropClick, className: "fixed inset-0 z-9998 bg-black/60 backdrop-blur-sm" }), jsxs("div", { ref: modalRef, className: cn('fixed inset-3 z-9999 mx-auto w-[min(95vw,520px)] overflow-y-auto rounded-2xl border', 'border-slate-200/70 bg-white/95 p-4 shadow-2xl backdrop-blur-sm', 'font-sans text-sm text-slate-700 dark:border-white/12 dark:bg-slate-950/95 dark:text-slate-200', 'sm:inset-auto md:left-2 sm:top-1 md:right-auto sm:w-[min(520px,95vw)] sm:p-5'), children: [jsx("header", { className: "mb-4", children: jsxs("div", { className: "flex items-start justify-between gap-3", children: [jsxs("div", { className: cn("flex items-center gap-2 text-base font-bold tracking-wider", themeIconColor), children: [jsx(ShieldUserIcon, { className: "size-4" }), translations.panel.title] }), jsxs("div", { className: "flex items-center gap-2", children: [
|
|
255
|
-
? cn('border-transparent text-white', themeButtonGradientClass, themeButtonGradientHoverClass)
|
|
256
|
-
: themedGhostButtonClass), "aria-pressed": panelMode === 'test', children: [jsx("span", { children: translations.panel.testModeLabel }), jsx("span", { className: cn('relative inline-flex h-5 w-9 items-center rounded-full transition-colors', panelMode === 'test'
|
|
257
|
-
? 'bg-white/25'
|
|
258
|
-
: 'bg-slate-300 dark:bg-slate-700'), children: jsx("span", { className: cn('inline-block size-4 rounded-full shadow-sm transition-transform', panelMode === 'test' ? 'bg-white' : 'bg-white dark:bg-slate-100', panelMode === 'test' ? 'translate-x-4' : 'translate-x-0.5') }) })] }), jsx("button", { type: "button", "aria-label": translations.panel.closeAriaLabel, 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", onClick: () => setIsOpen(false), children: jsx(XIcon, { className: "size-4" }) })] })] }) }), jsxs("section", { className: "space-y-1", children: [panelMode === 'info' ? (jsxs(Fragment, { children: [jsx(PanelSection, { icon: jsx(FingerprintIcon, { className: "size-4" }), title: translations.sections.user, rightInfo: jsx(StatusTag, { value: userStatus, translations: translations }), items: [
|
|
256
|
+
return (jsxs(Fragment, { children: [!isOpen && (jsx("button", { onClick: handleToggle, type: "button", "aria-label": translations.panel.toggleAriaLabel, className: cn('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', themeButtonGradientClass, themeButtonGradientHoverClass, 'text-white rounded-full shadow-lg hover:shadow-xl transition-all duration-300'), children: jsx(LightbulbIcon, { className: "size-6 text-white" }) })), isOpen && (jsxs(Fragment, { children: [jsx("div", { onClick: handleBackdropClick, className: "fixed inset-0 z-9998 bg-black/60 backdrop-blur-sm" }), jsxs("div", { ref: modalRef, className: cn('fixed inset-3 z-9999 mx-auto w-[min(95vw,520px)] overflow-y-auto rounded-2xl border', 'border-slate-200/70 bg-white/95 p-4 shadow-2xl backdrop-blur-sm', 'font-sans text-sm text-slate-700 dark:border-white/12 dark:bg-slate-950/95 dark:text-slate-200', 'sm:inset-auto md:left-2 sm:top-1 md:right-auto sm:w-[min(520px,95vw)] sm:p-5'), children: [jsx("header", { className: "mb-4", children: jsxs("div", { className: "flex items-start justify-between gap-3", children: [jsxs("div", { className: cn("flex items-center gap-2 text-base font-bold tracking-wider", themeIconColor), children: [jsx(ShieldUserIcon, { className: "size-4" }), translations.panel.title] }), jsxs("div", { className: "flex items-center gap-2", children: [jsx(XSwitchButton, { type: "button", checked: panelMode === 'test', onCheckedChange: (nextChecked) => setPanelMode(nextChecked ? 'test' : 'info'), checkedLabel: translations.panel.testModeLabel, uncheckedLabel: translations.panel.testModeLabel, size: "compact", className: "border px-2 py-1 text-[11px] shadow-sm" }), jsx("button", { type: "button", "aria-label": translations.panel.closeAriaLabel, 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", onClick: () => setIsOpen(false), children: jsx(XIcon, { className: "size-4" }) })] })] }) }), jsxs("section", { className: "space-y-1", children: [panelMode === 'info' ? (jsxs(Fragment, { children: [jsx(PanelSection, { icon: jsx(FingerprintIcon, { className: "size-4" }), title: translations.sections.user, rightInfo: jsx(StatusTag, { value: userStatus, translations: translations }), items: [
|
|
259
257
|
{ label: translations.labels.userId, value: jsx(CopyableText, { text: (xUser === null || xUser === void 0 ? void 0 : xUser.userId) || '' }) },
|
|
260
258
|
{ label: translations.labels.nickName, value: jsx(CopyableText, { text: (xUser === null || xUser === void 0 ? void 0 : xUser.userName) || '' }) },
|
|
261
259
|
{ label: translations.labels.fingerprintId, value: jsx(CopyableText, { text: (xUser === null || xUser === void 0 ? void 0 : xUser.fingerprintId) || fingerprintId || '' }) },
|
|
@@ -5,6 +5,7 @@ var adsAlertDialog = require('./ads-alert-dialog.js');
|
|
|
5
5
|
var confirmDialog = require('./confirm-dialog.js');
|
|
6
6
|
var highPriorityConfirmDialog = require('./high-priority-confirm-dialog.js');
|
|
7
7
|
var infoDialog = require('./info-dialog.js');
|
|
8
|
+
var undoableConfirmDialog = require('./undoable-confirm-dialog.js');
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
|
|
@@ -12,3 +13,4 @@ exports.AdsAlertDialog = adsAlertDialog.AdsAlertDialog;
|
|
|
12
13
|
exports.ConfirmDialog = confirmDialog.ConfirmDialog;
|
|
13
14
|
exports.HighPriorityConfirmDialog = highPriorityConfirmDialog.HighPriorityConfirmDialog;
|
|
14
15
|
exports.InfoDialog = infoDialog.InfoDialog;
|
|
16
|
+
exports.UndoableConfirmDialog = undoableConfirmDialog.UndoableConfirmDialog;
|
|
@@ -3,3 +3,4 @@ export { AdsAlertDialog } from './ads-alert-dialog.mjs';
|
|
|
3
3
|
export { ConfirmDialog } from './confirm-dialog.mjs';
|
|
4
4
|
export { HighPriorityConfirmDialog } from './high-priority-confirm-dialog.mjs';
|
|
5
5
|
export { InfoDialog } from './info-dialog.mjs';
|
|
6
|
+
export { UndoableConfirmDialog } from './undoable-confirm-dialog.mjs';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface UndoableConfirmDialogProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
title: React.ReactNode;
|
|
6
|
+
description: React.ReactNode;
|
|
7
|
+
pendingTitle?: React.ReactNode;
|
|
8
|
+
pendingDescription?: React.ReactNode;
|
|
9
|
+
cancelText?: string;
|
|
10
|
+
confirmText?: string;
|
|
11
|
+
undoText?: string;
|
|
12
|
+
countdownSeconds?: number;
|
|
13
|
+
onCancel?: () => void;
|
|
14
|
+
onConfirm: () => void | Promise<void>;
|
|
15
|
+
onUndo?: () => void;
|
|
16
|
+
}
|
|
17
|
+
export declare function UndoableConfirmDialog({ open, onOpenChange, title, description, pendingTitle, pendingDescription, cancelText, confirmText, undoText, countdownSeconds, onCancel, onConfirm, onUndo, }: UndoableConfirmDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var tslib = require('tslib');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
var React = require('react');
|
|
7
|
+
var icons = require('@windrun-huaiin/base-ui/icons');
|
|
8
|
+
var lib = require('@windrun-huaiin/base-ui/lib');
|
|
9
|
+
var ui = require('@windrun-huaiin/base-ui/ui');
|
|
10
|
+
var utils = require('@windrun-huaiin/lib/utils');
|
|
11
|
+
var dialogStyles = require('./dialog-styles.js');
|
|
12
|
+
|
|
13
|
+
function UndoableConfirmDialog({ open, onOpenChange, title, description, pendingTitle, pendingDescription, cancelText = 'Cancel', confirmText = 'Delete', undoText = 'Undo', countdownSeconds = 5, onCancel, onConfirm, onUndo, }) {
|
|
14
|
+
const safeCountdownSeconds = Math.max(1, Math.floor(countdownSeconds));
|
|
15
|
+
const [pending, setPending] = React.useState(false);
|
|
16
|
+
const [remainingSeconds, setRemainingSeconds] = React.useState(safeCountdownSeconds);
|
|
17
|
+
const [confirming, setConfirming] = React.useState(false);
|
|
18
|
+
const timeoutRef = React.useRef(null);
|
|
19
|
+
const intervalRef = React.useRef(null);
|
|
20
|
+
const clearTimers = React.useCallback(() => {
|
|
21
|
+
if (timeoutRef.current) {
|
|
22
|
+
window.clearTimeout(timeoutRef.current);
|
|
23
|
+
timeoutRef.current = null;
|
|
24
|
+
}
|
|
25
|
+
if (intervalRef.current) {
|
|
26
|
+
window.clearInterval(intervalRef.current);
|
|
27
|
+
intervalRef.current = null;
|
|
28
|
+
}
|
|
29
|
+
}, []);
|
|
30
|
+
const resetState = React.useCallback(() => {
|
|
31
|
+
clearTimers();
|
|
32
|
+
setPending(false);
|
|
33
|
+
setConfirming(false);
|
|
34
|
+
setRemainingSeconds(safeCountdownSeconds);
|
|
35
|
+
}, [clearTimers, safeCountdownSeconds]);
|
|
36
|
+
React.useEffect(() => {
|
|
37
|
+
if (open) {
|
|
38
|
+
setRemainingSeconds(safeCountdownSeconds);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
resetState();
|
|
42
|
+
}, [open, resetState, safeCountdownSeconds]);
|
|
43
|
+
React.useEffect(() => clearTimers, [clearTimers]);
|
|
44
|
+
const executeConfirm = React.useCallback(() => tslib.__awaiter(this, void 0, void 0, function* () {
|
|
45
|
+
clearTimers();
|
|
46
|
+
setConfirming(true);
|
|
47
|
+
try {
|
|
48
|
+
yield onConfirm();
|
|
49
|
+
onOpenChange(false);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
setConfirming(false);
|
|
53
|
+
}
|
|
54
|
+
}), [clearTimers, onConfirm, onOpenChange]);
|
|
55
|
+
const startCountdown = () => {
|
|
56
|
+
clearTimers();
|
|
57
|
+
setPending(true);
|
|
58
|
+
setRemainingSeconds(safeCountdownSeconds);
|
|
59
|
+
intervalRef.current = window.setInterval(() => {
|
|
60
|
+
setRemainingSeconds((current) => Math.max(0, current - 1));
|
|
61
|
+
}, 1000);
|
|
62
|
+
timeoutRef.current = window.setTimeout(() => {
|
|
63
|
+
void executeConfirm();
|
|
64
|
+
}, (safeCountdownSeconds + 1) * 1000);
|
|
65
|
+
};
|
|
66
|
+
const handleCancel = () => {
|
|
67
|
+
resetState();
|
|
68
|
+
onOpenChange(false);
|
|
69
|
+
onCancel === null || onCancel === void 0 ? void 0 : onCancel();
|
|
70
|
+
};
|
|
71
|
+
const handleUndo = () => {
|
|
72
|
+
resetState();
|
|
73
|
+
onOpenChange(false);
|
|
74
|
+
onUndo === null || onUndo === void 0 ? void 0 : onUndo();
|
|
75
|
+
};
|
|
76
|
+
const displayTitle = pending ? pendingTitle !== null && pendingTitle !== void 0 ? pendingTitle : title : title;
|
|
77
|
+
const displayDescription = pending ? pendingDescription !== null && pendingDescription !== void 0 ? pendingDescription : description : description;
|
|
78
|
+
return (jsxRuntime.jsx(ui.AlertDialog, { open: open, onOpenChange: (nextOpen) => {
|
|
79
|
+
if (!nextOpen) {
|
|
80
|
+
handleCancel();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
onOpenChange(nextOpen);
|
|
84
|
+
}, children: jsxRuntime.jsxs(ui.AlertDialogContent, { className: utils.cn(dialogStyles.dialogContentClass, 'border-red-300 dark:border-red-700'), overlayClassName: dialogStyles.dialogThemedOverlayClass, onOverlayClick: pending ? undefined : handleCancel, children: [jsxRuntime.jsxs("div", { className: dialogStyles.dialogHeaderClass, children: [jsxRuntime.jsx(ui.AlertDialogTitle, { asChild: true, children: jsxRuntime.jsxs("div", { className: dialogStyles.dialogTitleClass, children: [jsxRuntime.jsx("span", { className: "inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-red-100 text-red-600 ring-1 ring-red-200 dark:bg-red-950 dark:text-red-300 dark:ring-red-900", children: pending ? jsxRuntime.jsx(icons.Trash2Icon, { className: "size-5" }) : jsxRuntime.jsx(icons.CircleAlertIcon, { className: "size-5" }) }), jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: displayTitle })] }) }), jsxRuntime.jsx("button", { type: "button", className: dialogStyles.closeButtonClass, onClick: pending ? handleUndo : handleCancel, "aria-label": "Close", disabled: confirming, children: jsxRuntime.jsx(icons.XIcon, { className: "size-4" }) })] }), jsxRuntime.jsx(ui.AlertDialogDescription, { className: utils.cn(dialogStyles.dialogDescriptionClass, 'min-h-[44px]'), children: jsxRuntime.jsx("span", { children: displayDescription }) }), jsxRuntime.jsx("div", { className: "flex h-12 items-center justify-center py-1", children: jsxRuntime.jsxs("div", { className: "flex items-baseline justify-center gap-2", children: [jsxRuntime.jsx("span", { className: utils.cn('text-4xl font-black leading-none tabular-nums', pending && 'animate-bounce', lib.themeIconColor), children: pending ? remainingSeconds : safeCountdownSeconds }), jsxRuntime.jsx("span", { className: utils.cn('text-sm font-bold', lib.themeIconColor), children: "s" })] }) }), jsxRuntime.jsx("div", { className: utils.cn(dialogStyles.dialogFooterClass, 'min-h-[88px] sm:min-h-10 sm:items-center'), children: pending ? (jsxRuntime.jsxs("button", { type: "button", onClick: handleUndo, className: dialogStyles.secondaryButtonClass, disabled: confirming, children: [jsxRuntime.jsx(icons.Undo2Icon, { className: "mr-1.5 size-4" }), undoText] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("button", { type: "button", onClick: handleCancel, className: dialogStyles.secondaryButtonClass, children: cancelText }), jsxRuntime.jsx("button", { type: "button", onClick: startCountdown, className: dialogStyles.dangerButtonClass, children: confirmText })] })) })] }) }));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
exports.UndoableConfirmDialog = UndoableConfirmDialog;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { __awaiter } from 'tslib';
|
|
3
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
4
|
+
import React__default from 'react';
|
|
5
|
+
import { Trash2Icon, CircleAlertIcon, XIcon, Undo2Icon } from '@windrun-huaiin/base-ui/icons';
|
|
6
|
+
import { themeIconColor } from '@windrun-huaiin/base-ui/lib';
|
|
7
|
+
import { AlertDialog, AlertDialogContent, AlertDialogTitle, AlertDialogDescription } from '@windrun-huaiin/base-ui/ui';
|
|
8
|
+
import { cn } from '@windrun-huaiin/lib/utils';
|
|
9
|
+
import { dialogThemedOverlayClass, dialogHeaderClass, dialogTitleClass, closeButtonClass, dialogDescriptionClass, secondaryButtonClass, dangerButtonClass, dialogFooterClass, dialogContentClass } from './dialog-styles.mjs';
|
|
10
|
+
|
|
11
|
+
function UndoableConfirmDialog({ open, onOpenChange, title, description, pendingTitle, pendingDescription, cancelText = 'Cancel', confirmText = 'Delete', undoText = 'Undo', countdownSeconds = 5, onCancel, onConfirm, onUndo, }) {
|
|
12
|
+
const safeCountdownSeconds = Math.max(1, Math.floor(countdownSeconds));
|
|
13
|
+
const [pending, setPending] = React__default.useState(false);
|
|
14
|
+
const [remainingSeconds, setRemainingSeconds] = React__default.useState(safeCountdownSeconds);
|
|
15
|
+
const [confirming, setConfirming] = React__default.useState(false);
|
|
16
|
+
const timeoutRef = React__default.useRef(null);
|
|
17
|
+
const intervalRef = React__default.useRef(null);
|
|
18
|
+
const clearTimers = React__default.useCallback(() => {
|
|
19
|
+
if (timeoutRef.current) {
|
|
20
|
+
window.clearTimeout(timeoutRef.current);
|
|
21
|
+
timeoutRef.current = null;
|
|
22
|
+
}
|
|
23
|
+
if (intervalRef.current) {
|
|
24
|
+
window.clearInterval(intervalRef.current);
|
|
25
|
+
intervalRef.current = null;
|
|
26
|
+
}
|
|
27
|
+
}, []);
|
|
28
|
+
const resetState = React__default.useCallback(() => {
|
|
29
|
+
clearTimers();
|
|
30
|
+
setPending(false);
|
|
31
|
+
setConfirming(false);
|
|
32
|
+
setRemainingSeconds(safeCountdownSeconds);
|
|
33
|
+
}, [clearTimers, safeCountdownSeconds]);
|
|
34
|
+
React__default.useEffect(() => {
|
|
35
|
+
if (open) {
|
|
36
|
+
setRemainingSeconds(safeCountdownSeconds);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
resetState();
|
|
40
|
+
}, [open, resetState, safeCountdownSeconds]);
|
|
41
|
+
React__default.useEffect(() => clearTimers, [clearTimers]);
|
|
42
|
+
const executeConfirm = React__default.useCallback(() => __awaiter(this, void 0, void 0, function* () {
|
|
43
|
+
clearTimers();
|
|
44
|
+
setConfirming(true);
|
|
45
|
+
try {
|
|
46
|
+
yield onConfirm();
|
|
47
|
+
onOpenChange(false);
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
setConfirming(false);
|
|
51
|
+
}
|
|
52
|
+
}), [clearTimers, onConfirm, onOpenChange]);
|
|
53
|
+
const startCountdown = () => {
|
|
54
|
+
clearTimers();
|
|
55
|
+
setPending(true);
|
|
56
|
+
setRemainingSeconds(safeCountdownSeconds);
|
|
57
|
+
intervalRef.current = window.setInterval(() => {
|
|
58
|
+
setRemainingSeconds((current) => Math.max(0, current - 1));
|
|
59
|
+
}, 1000);
|
|
60
|
+
timeoutRef.current = window.setTimeout(() => {
|
|
61
|
+
void executeConfirm();
|
|
62
|
+
}, (safeCountdownSeconds + 1) * 1000);
|
|
63
|
+
};
|
|
64
|
+
const handleCancel = () => {
|
|
65
|
+
resetState();
|
|
66
|
+
onOpenChange(false);
|
|
67
|
+
onCancel === null || onCancel === void 0 ? void 0 : onCancel();
|
|
68
|
+
};
|
|
69
|
+
const handleUndo = () => {
|
|
70
|
+
resetState();
|
|
71
|
+
onOpenChange(false);
|
|
72
|
+
onUndo === null || onUndo === void 0 ? void 0 : onUndo();
|
|
73
|
+
};
|
|
74
|
+
const displayTitle = pending ? pendingTitle !== null && pendingTitle !== void 0 ? pendingTitle : title : title;
|
|
75
|
+
const displayDescription = pending ? pendingDescription !== null && pendingDescription !== void 0 ? pendingDescription : description : description;
|
|
76
|
+
return (jsx(AlertDialog, { open: open, onOpenChange: (nextOpen) => {
|
|
77
|
+
if (!nextOpen) {
|
|
78
|
+
handleCancel();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
onOpenChange(nextOpen);
|
|
82
|
+
}, children: jsxs(AlertDialogContent, { className: cn(dialogContentClass, 'border-red-300 dark:border-red-700'), overlayClassName: dialogThemedOverlayClass, onOverlayClick: pending ? undefined : handleCancel, children: [jsxs("div", { className: dialogHeaderClass, children: [jsx(AlertDialogTitle, { asChild: true, children: jsxs("div", { className: dialogTitleClass, children: [jsx("span", { className: "inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-red-100 text-red-600 ring-1 ring-red-200 dark:bg-red-950 dark:text-red-300 dark:ring-red-900", children: pending ? jsx(Trash2Icon, { className: "size-5" }) : jsx(CircleAlertIcon, { className: "size-5" }) }), jsx("span", { className: "min-w-0 truncate", children: displayTitle })] }) }), jsx("button", { type: "button", className: closeButtonClass, onClick: pending ? handleUndo : handleCancel, "aria-label": "Close", disabled: confirming, children: jsx(XIcon, { className: "size-4" }) })] }), jsx(AlertDialogDescription, { className: cn(dialogDescriptionClass, 'min-h-[44px]'), children: jsx("span", { children: displayDescription }) }), jsx("div", { className: "flex h-12 items-center justify-center py-1", children: jsxs("div", { className: "flex items-baseline justify-center gap-2", children: [jsx("span", { className: cn('text-4xl font-black leading-none tabular-nums', pending && 'animate-bounce', themeIconColor), children: pending ? remainingSeconds : safeCountdownSeconds }), jsx("span", { className: cn('text-sm font-bold', themeIconColor), children: "s" })] }) }), jsx("div", { className: cn(dialogFooterClass, 'min-h-[88px] sm:min-h-10 sm:items-center'), children: pending ? (jsxs("button", { type: "button", onClick: handleUndo, className: secondaryButtonClass, disabled: confirming, children: [jsx(Undo2Icon, { className: "mr-1.5 size-4" }), undoText] })) : (jsxs(Fragment, { children: [jsx("button", { type: "button", onClick: handleCancel, className: secondaryButtonClass, children: cancelText }), jsx("button", { type: "button", onClick: startCountdown, className: dangerButtonClass, children: confirmText })] })) })] }) }));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { UndoableConfirmDialog };
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
var gradientButton = require('./gradient-button.js');
|
|
5
5
|
var xButton = require('./x-button.js');
|
|
6
|
+
var xSwitchButton = require('./x-switch-button.js');
|
|
6
7
|
var xToggleButton = require('./x-toggle-button.js');
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
exports.GradientButton = gradientButton.GradientButton;
|
|
11
12
|
exports.XButton = xButton.XButton;
|
|
13
|
+
exports.XSwitchButton = xSwitchButton.XSwitchButton;
|
|
12
14
|
exports.XToggleButton = xToggleButton.XToggleButton;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export type XSwitchButtonSize = 'default' | 'compact';
|
|
3
|
+
export type XSwitchButtonProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onChange' | 'value' | 'defaultValue' | 'children'> & {
|
|
4
|
+
checked?: boolean;
|
|
5
|
+
defaultChecked?: boolean;
|
|
6
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
7
|
+
checkedLabel?: React.ReactNode;
|
|
8
|
+
uncheckedLabel?: React.ReactNode;
|
|
9
|
+
labelPosition?: 'left' | 'right';
|
|
10
|
+
size?: XSwitchButtonSize;
|
|
11
|
+
className?: string;
|
|
12
|
+
labelClassName?: string;
|
|
13
|
+
trackClassName?: string;
|
|
14
|
+
thumbClassName?: string;
|
|
15
|
+
checkedClassName?: string;
|
|
16
|
+
uncheckedClassName?: string;
|
|
17
|
+
checkedTrackClassName?: string;
|
|
18
|
+
uncheckedTrackClassName?: string;
|
|
19
|
+
checkedThumbClassName?: string;
|
|
20
|
+
uncheckedThumbClassName?: string;
|
|
21
|
+
};
|
|
22
|
+
export declare function XSwitchButton({ checked, defaultChecked, onCheckedChange, checkedLabel, uncheckedLabel, labelPosition, size, className, labelClassName, trackClassName, thumbClassName, checkedClassName, uncheckedClassName, checkedTrackClassName, uncheckedTrackClassName, checkedThumbClassName, uncheckedThumbClassName, disabled, type, onClick, ...props }: XSwitchButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var tslib = require('tslib');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
var React = require('react');
|
|
7
|
+
var lib = require('@windrun-huaiin/base-ui/lib');
|
|
8
|
+
var utils = require('@windrun-huaiin/lib/utils');
|
|
9
|
+
|
|
10
|
+
function _interopNamespaceDefault(e) {
|
|
11
|
+
var n = Object.create(null);
|
|
12
|
+
if (e) {
|
|
13
|
+
Object.keys(e).forEach(function (k) {
|
|
14
|
+
if (k !== 'default') {
|
|
15
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
16
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
get: function () { return e[k]; }
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
n.default = e;
|
|
24
|
+
return Object.freeze(n);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
|
|
28
|
+
|
|
29
|
+
function XSwitchButton(_a) {
|
|
30
|
+
var { checked, defaultChecked = false, onCheckedChange, checkedLabel, uncheckedLabel, labelPosition = 'left', size = 'default', className, labelClassName, trackClassName, thumbClassName, checkedClassName, uncheckedClassName, checkedTrackClassName, uncheckedTrackClassName, checkedThumbClassName, uncheckedThumbClassName, disabled, type = 'button', onClick } = _a, props = tslib.__rest(_a, ["checked", "defaultChecked", "onCheckedChange", "checkedLabel", "uncheckedLabel", "labelPosition", "size", "className", "labelClassName", "trackClassName", "thumbClassName", "checkedClassName", "uncheckedClassName", "checkedTrackClassName", "uncheckedTrackClassName", "checkedThumbClassName", "uncheckedThumbClassName", "disabled", "type", "onClick"]);
|
|
31
|
+
const isControlled = checked !== undefined;
|
|
32
|
+
const [internalChecked, setInternalChecked] = React__namespace.useState(defaultChecked);
|
|
33
|
+
const active = isControlled ? checked : internalChecked;
|
|
34
|
+
const currentLabel = active ? checkedLabel : uncheckedLabel;
|
|
35
|
+
const hasLabel = currentLabel !== undefined && currentLabel !== null && currentLabel !== false;
|
|
36
|
+
function handleClick(event) {
|
|
37
|
+
onClick === null || onClick === void 0 ? void 0 : onClick(event);
|
|
38
|
+
if (event.defaultPrevented || disabled) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const nextChecked = !active;
|
|
42
|
+
if (!isControlled) {
|
|
43
|
+
setInternalChecked(nextChecked);
|
|
44
|
+
}
|
|
45
|
+
onCheckedChange === null || onCheckedChange === void 0 ? void 0 : onCheckedChange(nextChecked);
|
|
46
|
+
}
|
|
47
|
+
const trackSizeClass = size === 'compact' ? 'h-5 w-9' : 'h-6 w-11';
|
|
48
|
+
const thumbSizeClass = size === 'compact' ? 'size-4' : 'size-5';
|
|
49
|
+
const thumbTranslateClass = active
|
|
50
|
+
? size === 'compact'
|
|
51
|
+
? 'translate-x-4'
|
|
52
|
+
: 'translate-x-5'
|
|
53
|
+
: 'translate-x-0.5';
|
|
54
|
+
const labelNode = hasLabel ? (jsxRuntime.jsx("span", { className: utils.cn('min-w-0', labelClassName), children: currentLabel })) : null;
|
|
55
|
+
const defaultCheckedClassName = utils.cn('border-transparent text-white shadow-sm', lib.themeButtonGradientClass, lib.themeButtonGradientHoverClass);
|
|
56
|
+
const defaultUncheckedClassName = utils.cn('border-slate-200 bg-white/90 text-slate-700 shadow-sm hover:border-current hover:bg-slate-50', 'dark:border-white/20 dark:bg-slate-900/90 dark:text-slate-200 dark:shadow-white/5 dark:hover:bg-slate-800', lib.themeIconColor);
|
|
57
|
+
const trackNode = (jsxRuntime.jsx("span", { "aria-hidden": "true", className: utils.cn('relative inline-flex shrink-0 items-center rounded-full transition-colors', trackSizeClass, active ? 'bg-white/25' : 'bg-slate-300 dark:bg-slate-700', trackClassName, active ? checkedTrackClassName : uncheckedTrackClassName), children: jsxRuntime.jsx("span", { className: utils.cn('inline-block rounded-full bg-white shadow-sm transition-transform dark:bg-slate-100', thumbSizeClass, thumbTranslateClass, thumbClassName, active ? checkedThumbClassName : uncheckedThumbClassName) }) }));
|
|
58
|
+
return (jsxRuntime.jsx("button", Object.assign({ type: type, role: "switch", "aria-checked": active, disabled: disabled, onClick: handleClick, className: utils.cn('inline-flex items-center gap-2 rounded-full font-semibold transition-all duration-200', active ? defaultCheckedClassName : defaultUncheckedClassName, className, active
|
|
59
|
+
? checkedClassName
|
|
60
|
+
: uncheckedClassName, disabled && 'cursor-not-allowed opacity-60') }, props, { children: labelPosition === 'left' ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [labelNode, trackNode] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [trackNode, labelNode] })) })));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
exports.XSwitchButton = XSwitchButton;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { __rest } from 'tslib';
|
|
3
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { themeButtonGradientClass, themeButtonGradientHoverClass, themeIconColor } from '@windrun-huaiin/base-ui/lib';
|
|
6
|
+
import { cn } from '@windrun-huaiin/lib/utils';
|
|
7
|
+
|
|
8
|
+
function XSwitchButton(_a) {
|
|
9
|
+
var { checked, defaultChecked = false, onCheckedChange, checkedLabel, uncheckedLabel, labelPosition = 'left', size = 'default', className, labelClassName, trackClassName, thumbClassName, checkedClassName, uncheckedClassName, checkedTrackClassName, uncheckedTrackClassName, checkedThumbClassName, uncheckedThumbClassName, disabled, type = 'button', onClick } = _a, props = __rest(_a, ["checked", "defaultChecked", "onCheckedChange", "checkedLabel", "uncheckedLabel", "labelPosition", "size", "className", "labelClassName", "trackClassName", "thumbClassName", "checkedClassName", "uncheckedClassName", "checkedTrackClassName", "uncheckedTrackClassName", "checkedThumbClassName", "uncheckedThumbClassName", "disabled", "type", "onClick"]);
|
|
10
|
+
const isControlled = checked !== undefined;
|
|
11
|
+
const [internalChecked, setInternalChecked] = React.useState(defaultChecked);
|
|
12
|
+
const active = isControlled ? checked : internalChecked;
|
|
13
|
+
const currentLabel = active ? checkedLabel : uncheckedLabel;
|
|
14
|
+
const hasLabel = currentLabel !== undefined && currentLabel !== null && currentLabel !== false;
|
|
15
|
+
function handleClick(event) {
|
|
16
|
+
onClick === null || onClick === void 0 ? void 0 : onClick(event);
|
|
17
|
+
if (event.defaultPrevented || disabled) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const nextChecked = !active;
|
|
21
|
+
if (!isControlled) {
|
|
22
|
+
setInternalChecked(nextChecked);
|
|
23
|
+
}
|
|
24
|
+
onCheckedChange === null || onCheckedChange === void 0 ? void 0 : onCheckedChange(nextChecked);
|
|
25
|
+
}
|
|
26
|
+
const trackSizeClass = size === 'compact' ? 'h-5 w-9' : 'h-6 w-11';
|
|
27
|
+
const thumbSizeClass = size === 'compact' ? 'size-4' : 'size-5';
|
|
28
|
+
const thumbTranslateClass = active
|
|
29
|
+
? size === 'compact'
|
|
30
|
+
? 'translate-x-4'
|
|
31
|
+
: 'translate-x-5'
|
|
32
|
+
: 'translate-x-0.5';
|
|
33
|
+
const labelNode = hasLabel ? (jsx("span", { className: cn('min-w-0', labelClassName), children: currentLabel })) : null;
|
|
34
|
+
const defaultCheckedClassName = cn('border-transparent text-white shadow-sm', themeButtonGradientClass, themeButtonGradientHoverClass);
|
|
35
|
+
const defaultUncheckedClassName = cn('border-slate-200 bg-white/90 text-slate-700 shadow-sm hover:border-current hover:bg-slate-50', 'dark:border-white/20 dark:bg-slate-900/90 dark:text-slate-200 dark:shadow-white/5 dark:hover:bg-slate-800', themeIconColor);
|
|
36
|
+
const trackNode = (jsx("span", { "aria-hidden": "true", className: cn('relative inline-flex shrink-0 items-center rounded-full transition-colors', trackSizeClass, active ? 'bg-white/25' : 'bg-slate-300 dark:bg-slate-700', trackClassName, active ? checkedTrackClassName : uncheckedTrackClassName), children: jsx("span", { className: cn('inline-block rounded-full bg-white shadow-sm transition-transform dark:bg-slate-100', thumbSizeClass, thumbTranslateClass, thumbClassName, active ? checkedThumbClassName : uncheckedThumbClassName) }) }));
|
|
37
|
+
return (jsx("button", Object.assign({ type: type, role: "switch", "aria-checked": active, disabled: disabled, onClick: handleClick, className: cn('inline-flex items-center gap-2 rounded-full font-semibold transition-all duration-200', active ? defaultCheckedClassName : defaultUncheckedClassName, className, active
|
|
38
|
+
? checkedClassName
|
|
39
|
+
: uncheckedClassName, disabled && 'cursor-not-allowed opacity-60') }, props, { children: labelPosition === 'left' ? (jsxs(Fragment, { children: [labelNode, trackNode] })) : (jsxs(Fragment, { children: [trackNode, labelNode] })) })));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { XSwitchButton };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@windrun-huaiin/third-ui",
|
|
3
|
-
"version": "29.0.
|
|
3
|
+
"version": "29.0.3",
|
|
4
4
|
"description": "Third-party integrated UI components for windrun-huaiin projects",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./clerk": {
|
|
@@ -227,7 +227,7 @@
|
|
|
227
227
|
"tslib": "^2.8.1",
|
|
228
228
|
"unified": "^11.0.5",
|
|
229
229
|
"zod": "^4.3.6",
|
|
230
|
-
"@windrun-huaiin/base-ui": "^29.0.
|
|
230
|
+
"@windrun-huaiin/base-ui": "^29.0.1",
|
|
231
231
|
"@windrun-huaiin/lib": "^29.0.0",
|
|
232
232
|
"@windrun-huaiin/contracts": "^29.0.0"
|
|
233
233
|
},
|
|
@@ -20,6 +20,7 @@ import React, { createContext, useContext, useEffect, useMemo, useRef, useState
|
|
|
20
20
|
import type { FingerprintContextType, FingerprintProviderProps } from './types';
|
|
21
21
|
import { useFingerprint } from './use-fingerprint';
|
|
22
22
|
import { CopyableText } from '@windrun-huaiin/base-ui/ui';
|
|
23
|
+
import { XSwitchButton } from '../../main/buttons';
|
|
23
24
|
import { createFingerprintHeaders } from './fingerprint-client';
|
|
24
25
|
import {
|
|
25
26
|
getOrCreateDebugFingerprintOverride,
|
|
@@ -436,35 +437,15 @@ export function FingerprintStatus() {
|
|
|
436
437
|
{translations.panel.title}
|
|
437
438
|
</div>
|
|
438
439
|
<div className="flex items-center gap-2">
|
|
439
|
-
<
|
|
440
|
+
<XSwitchButton
|
|
440
441
|
type="button"
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
aria-pressed={panelMode === 'test'}
|
|
449
|
-
>
|
|
450
|
-
<span>{translations.panel.testModeLabel}</span>
|
|
451
|
-
<span
|
|
452
|
-
className={cn(
|
|
453
|
-
'relative inline-flex h-5 w-9 items-center rounded-full transition-colors',
|
|
454
|
-
panelMode === 'test'
|
|
455
|
-
? 'bg-white/25'
|
|
456
|
-
: 'bg-slate-300 dark:bg-slate-700'
|
|
457
|
-
)}
|
|
458
|
-
>
|
|
459
|
-
<span
|
|
460
|
-
className={cn(
|
|
461
|
-
'inline-block size-4 rounded-full shadow-sm transition-transform',
|
|
462
|
-
panelMode === 'test' ? 'bg-white' : 'bg-white dark:bg-slate-100',
|
|
463
|
-
panelMode === 'test' ? 'translate-x-4' : 'translate-x-0.5'
|
|
464
|
-
)}
|
|
465
|
-
/>
|
|
466
|
-
</span>
|
|
467
|
-
</button>
|
|
442
|
+
checked={panelMode === 'test'}
|
|
443
|
+
onCheckedChange={(nextChecked) => setPanelMode(nextChecked ? 'test' : 'info')}
|
|
444
|
+
checkedLabel={translations.panel.testModeLabel}
|
|
445
|
+
uncheckedLabel={translations.panel.testModeLabel}
|
|
446
|
+
size="compact"
|
|
447
|
+
className="border px-2 py-1 text-[11px] shadow-sm"
|
|
448
|
+
/>
|
|
468
449
|
<button
|
|
469
450
|
type="button"
|
|
470
451
|
aria-label={translations.panel.closeAriaLabel}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { CircleAlertIcon, Trash2Icon, Undo2Icon, XIcon } from '@windrun-huaiin/base-ui/icons';
|
|
5
|
+
import { themeIconColor } from '@windrun-huaiin/base-ui/lib';
|
|
6
|
+
import {
|
|
7
|
+
AlertDialog,
|
|
8
|
+
AlertDialogContent,
|
|
9
|
+
AlertDialogDescription,
|
|
10
|
+
AlertDialogTitle,
|
|
11
|
+
} from '@windrun-huaiin/base-ui/ui';
|
|
12
|
+
import { cn } from '@windrun-huaiin/lib/utils';
|
|
13
|
+
import {
|
|
14
|
+
closeButtonClass,
|
|
15
|
+
dangerButtonClass,
|
|
16
|
+
dialogContentClass,
|
|
17
|
+
dialogDescriptionClass,
|
|
18
|
+
dialogFooterClass,
|
|
19
|
+
dialogHeaderClass,
|
|
20
|
+
dialogThemedOverlayClass,
|
|
21
|
+
dialogTitleClass,
|
|
22
|
+
secondaryButtonClass,
|
|
23
|
+
} from './dialog-styles';
|
|
24
|
+
|
|
25
|
+
export interface UndoableConfirmDialogProps {
|
|
26
|
+
open: boolean;
|
|
27
|
+
onOpenChange: (open: boolean) => void;
|
|
28
|
+
title: React.ReactNode;
|
|
29
|
+
description: React.ReactNode;
|
|
30
|
+
pendingTitle?: React.ReactNode;
|
|
31
|
+
pendingDescription?: React.ReactNode;
|
|
32
|
+
cancelText?: string;
|
|
33
|
+
confirmText?: string;
|
|
34
|
+
undoText?: string;
|
|
35
|
+
countdownSeconds?: number;
|
|
36
|
+
onCancel?: () => void;
|
|
37
|
+
onConfirm: () => void | Promise<void>;
|
|
38
|
+
onUndo?: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function UndoableConfirmDialog({
|
|
42
|
+
open,
|
|
43
|
+
onOpenChange,
|
|
44
|
+
title,
|
|
45
|
+
description,
|
|
46
|
+
pendingTitle,
|
|
47
|
+
pendingDescription,
|
|
48
|
+
cancelText = 'Cancel',
|
|
49
|
+
confirmText = 'Delete',
|
|
50
|
+
undoText = 'Undo',
|
|
51
|
+
countdownSeconds = 5,
|
|
52
|
+
onCancel,
|
|
53
|
+
onConfirm,
|
|
54
|
+
onUndo,
|
|
55
|
+
}: UndoableConfirmDialogProps) {
|
|
56
|
+
const safeCountdownSeconds = Math.max(1, Math.floor(countdownSeconds));
|
|
57
|
+
const [pending, setPending] = React.useState(false);
|
|
58
|
+
const [remainingSeconds, setRemainingSeconds] = React.useState(safeCountdownSeconds);
|
|
59
|
+
const [confirming, setConfirming] = React.useState(false);
|
|
60
|
+
const timeoutRef = React.useRef<number | null>(null);
|
|
61
|
+
const intervalRef = React.useRef<number | null>(null);
|
|
62
|
+
|
|
63
|
+
const clearTimers = React.useCallback(() => {
|
|
64
|
+
if (timeoutRef.current) {
|
|
65
|
+
window.clearTimeout(timeoutRef.current);
|
|
66
|
+
timeoutRef.current = null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (intervalRef.current) {
|
|
70
|
+
window.clearInterval(intervalRef.current);
|
|
71
|
+
intervalRef.current = null;
|
|
72
|
+
}
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
const resetState = React.useCallback(() => {
|
|
76
|
+
clearTimers();
|
|
77
|
+
setPending(false);
|
|
78
|
+
setConfirming(false);
|
|
79
|
+
setRemainingSeconds(safeCountdownSeconds);
|
|
80
|
+
}, [clearTimers, safeCountdownSeconds]);
|
|
81
|
+
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
if (open) {
|
|
84
|
+
setRemainingSeconds(safeCountdownSeconds);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
resetState();
|
|
89
|
+
}, [open, resetState, safeCountdownSeconds]);
|
|
90
|
+
|
|
91
|
+
React.useEffect(() => clearTimers, [clearTimers]);
|
|
92
|
+
|
|
93
|
+
const executeConfirm = React.useCallback(async () => {
|
|
94
|
+
clearTimers();
|
|
95
|
+
setConfirming(true);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
await onConfirm();
|
|
99
|
+
onOpenChange(false);
|
|
100
|
+
} finally {
|
|
101
|
+
setConfirming(false);
|
|
102
|
+
}
|
|
103
|
+
}, [clearTimers, onConfirm, onOpenChange]);
|
|
104
|
+
|
|
105
|
+
const startCountdown = () => {
|
|
106
|
+
clearTimers();
|
|
107
|
+
setPending(true);
|
|
108
|
+
setRemainingSeconds(safeCountdownSeconds);
|
|
109
|
+
|
|
110
|
+
intervalRef.current = window.setInterval(() => {
|
|
111
|
+
setRemainingSeconds((current) => Math.max(0, current - 1));
|
|
112
|
+
}, 1000);
|
|
113
|
+
|
|
114
|
+
timeoutRef.current = window.setTimeout(() => {
|
|
115
|
+
void executeConfirm();
|
|
116
|
+
}, (safeCountdownSeconds + 1) * 1000);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handleCancel = () => {
|
|
120
|
+
resetState();
|
|
121
|
+
onOpenChange(false);
|
|
122
|
+
onCancel?.();
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const handleUndo = () => {
|
|
126
|
+
resetState();
|
|
127
|
+
onOpenChange(false);
|
|
128
|
+
onUndo?.();
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const displayTitle = pending ? pendingTitle ?? title : title;
|
|
132
|
+
const displayDescription = pending ? pendingDescription ?? description : description;
|
|
133
|
+
return (
|
|
134
|
+
<AlertDialog open={open} onOpenChange={(nextOpen) => {
|
|
135
|
+
if (!nextOpen) {
|
|
136
|
+
handleCancel();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
onOpenChange(nextOpen);
|
|
141
|
+
}}>
|
|
142
|
+
<AlertDialogContent
|
|
143
|
+
className={cn(dialogContentClass, 'border-red-300 dark:border-red-700')}
|
|
144
|
+
overlayClassName={dialogThemedOverlayClass}
|
|
145
|
+
onOverlayClick={pending ? undefined : handleCancel}
|
|
146
|
+
>
|
|
147
|
+
<div className={dialogHeaderClass}>
|
|
148
|
+
<AlertDialogTitle asChild>
|
|
149
|
+
<div className={dialogTitleClass}>
|
|
150
|
+
<span className="inline-flex size-9 shrink-0 items-center justify-center rounded-full bg-red-100 text-red-600 ring-1 ring-red-200 dark:bg-red-950 dark:text-red-300 dark:ring-red-900">
|
|
151
|
+
{pending ? <Trash2Icon className="size-5" /> : <CircleAlertIcon className="size-5" />}
|
|
152
|
+
</span>
|
|
153
|
+
<span className="min-w-0 truncate">{displayTitle}</span>
|
|
154
|
+
</div>
|
|
155
|
+
</AlertDialogTitle>
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
className={closeButtonClass}
|
|
159
|
+
onClick={pending ? handleUndo : handleCancel}
|
|
160
|
+
aria-label="Close"
|
|
161
|
+
disabled={confirming}
|
|
162
|
+
>
|
|
163
|
+
<XIcon className="size-4" />
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<AlertDialogDescription className={cn(dialogDescriptionClass, 'min-h-[44px]')}>
|
|
168
|
+
<span>{displayDescription}</span>
|
|
169
|
+
</AlertDialogDescription>
|
|
170
|
+
|
|
171
|
+
<div className="flex h-12 items-center justify-center py-1">
|
|
172
|
+
<div className="flex items-baseline justify-center gap-2">
|
|
173
|
+
<span className={cn('text-4xl font-black leading-none tabular-nums', pending && 'animate-bounce', themeIconColor)}>
|
|
174
|
+
{pending ? remainingSeconds : safeCountdownSeconds}
|
|
175
|
+
</span>
|
|
176
|
+
<span className={cn('text-sm font-bold', themeIconColor)}>
|
|
177
|
+
s
|
|
178
|
+
</span>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div className={cn(dialogFooterClass, 'min-h-[88px] sm:min-h-10 sm:items-center')}>
|
|
183
|
+
{pending ? (
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
onClick={handleUndo}
|
|
187
|
+
className={secondaryButtonClass}
|
|
188
|
+
disabled={confirming}
|
|
189
|
+
>
|
|
190
|
+
<Undo2Icon className="mr-1.5 size-4" />
|
|
191
|
+
{undoText}
|
|
192
|
+
</button>
|
|
193
|
+
) : (
|
|
194
|
+
<>
|
|
195
|
+
<button
|
|
196
|
+
type="button"
|
|
197
|
+
onClick={handleCancel}
|
|
198
|
+
className={secondaryButtonClass}
|
|
199
|
+
>
|
|
200
|
+
{cancelText}
|
|
201
|
+
</button>
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
onClick={startCountdown}
|
|
205
|
+
className={dangerButtonClass}
|
|
206
|
+
>
|
|
207
|
+
{confirmText}
|
|
208
|
+
</button>
|
|
209
|
+
</>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
</AlertDialogContent>
|
|
213
|
+
</AlertDialog>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import {
|
|
5
|
+
themeButtonGradientClass,
|
|
6
|
+
themeButtonGradientHoverClass,
|
|
7
|
+
themeIconColor,
|
|
8
|
+
} from '@windrun-huaiin/base-ui/lib';
|
|
9
|
+
import { cn } from '@windrun-huaiin/lib/utils';
|
|
10
|
+
|
|
11
|
+
export type XSwitchButtonSize = 'default' | 'compact';
|
|
12
|
+
|
|
13
|
+
export type XSwitchButtonProps = Omit<
|
|
14
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
15
|
+
'onChange' | 'value' | 'defaultValue' | 'children'
|
|
16
|
+
> & {
|
|
17
|
+
checked?: boolean;
|
|
18
|
+
defaultChecked?: boolean;
|
|
19
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
20
|
+
checkedLabel?: React.ReactNode;
|
|
21
|
+
uncheckedLabel?: React.ReactNode;
|
|
22
|
+
labelPosition?: 'left' | 'right';
|
|
23
|
+
size?: XSwitchButtonSize;
|
|
24
|
+
className?: string;
|
|
25
|
+
labelClassName?: string;
|
|
26
|
+
trackClassName?: string;
|
|
27
|
+
thumbClassName?: string;
|
|
28
|
+
checkedClassName?: string;
|
|
29
|
+
uncheckedClassName?: string;
|
|
30
|
+
checkedTrackClassName?: string;
|
|
31
|
+
uncheckedTrackClassName?: string;
|
|
32
|
+
checkedThumbClassName?: string;
|
|
33
|
+
uncheckedThumbClassName?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function XSwitchButton({
|
|
37
|
+
checked,
|
|
38
|
+
defaultChecked = false,
|
|
39
|
+
onCheckedChange,
|
|
40
|
+
checkedLabel,
|
|
41
|
+
uncheckedLabel,
|
|
42
|
+
labelPosition = 'left',
|
|
43
|
+
size = 'default',
|
|
44
|
+
className,
|
|
45
|
+
labelClassName,
|
|
46
|
+
trackClassName,
|
|
47
|
+
thumbClassName,
|
|
48
|
+
checkedClassName,
|
|
49
|
+
uncheckedClassName,
|
|
50
|
+
checkedTrackClassName,
|
|
51
|
+
uncheckedTrackClassName,
|
|
52
|
+
checkedThumbClassName,
|
|
53
|
+
uncheckedThumbClassName,
|
|
54
|
+
disabled,
|
|
55
|
+
type = 'button',
|
|
56
|
+
onClick,
|
|
57
|
+
...props
|
|
58
|
+
}: XSwitchButtonProps) {
|
|
59
|
+
const isControlled = checked !== undefined;
|
|
60
|
+
const [internalChecked, setInternalChecked] = React.useState(defaultChecked);
|
|
61
|
+
const active = isControlled ? checked : internalChecked;
|
|
62
|
+
const currentLabel = active ? checkedLabel : uncheckedLabel;
|
|
63
|
+
const hasLabel = currentLabel !== undefined && currentLabel !== null && currentLabel !== false;
|
|
64
|
+
|
|
65
|
+
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
|
|
66
|
+
onClick?.(event);
|
|
67
|
+
|
|
68
|
+
if (event.defaultPrevented || disabled) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const nextChecked = !active;
|
|
73
|
+
|
|
74
|
+
if (!isControlled) {
|
|
75
|
+
setInternalChecked(nextChecked);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
onCheckedChange?.(nextChecked);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const trackSizeClass = size === 'compact' ? 'h-5 w-9' : 'h-6 w-11';
|
|
82
|
+
const thumbSizeClass = size === 'compact' ? 'size-4' : 'size-5';
|
|
83
|
+
const thumbTranslateClass = active
|
|
84
|
+
? size === 'compact'
|
|
85
|
+
? 'translate-x-4'
|
|
86
|
+
: 'translate-x-5'
|
|
87
|
+
: 'translate-x-0.5';
|
|
88
|
+
|
|
89
|
+
const labelNode = hasLabel ? (
|
|
90
|
+
<span className={cn('min-w-0', labelClassName)}>
|
|
91
|
+
{currentLabel}
|
|
92
|
+
</span>
|
|
93
|
+
) : null;
|
|
94
|
+
|
|
95
|
+
const defaultCheckedClassName = cn(
|
|
96
|
+
'border-transparent text-white shadow-sm',
|
|
97
|
+
themeButtonGradientClass,
|
|
98
|
+
themeButtonGradientHoverClass
|
|
99
|
+
);
|
|
100
|
+
const defaultUncheckedClassName = cn(
|
|
101
|
+
'border-slate-200 bg-white/90 text-slate-700 shadow-sm hover:border-current hover:bg-slate-50',
|
|
102
|
+
'dark:border-white/20 dark:bg-slate-900/90 dark:text-slate-200 dark:shadow-white/5 dark:hover:bg-slate-800',
|
|
103
|
+
themeIconColor
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const trackNode = (
|
|
107
|
+
<span
|
|
108
|
+
aria-hidden="true"
|
|
109
|
+
className={cn(
|
|
110
|
+
'relative inline-flex shrink-0 items-center rounded-full transition-colors',
|
|
111
|
+
trackSizeClass,
|
|
112
|
+
active ? 'bg-white/25' : 'bg-slate-300 dark:bg-slate-700',
|
|
113
|
+
trackClassName,
|
|
114
|
+
active ? checkedTrackClassName : uncheckedTrackClassName
|
|
115
|
+
)}
|
|
116
|
+
>
|
|
117
|
+
<span
|
|
118
|
+
className={cn(
|
|
119
|
+
'inline-block rounded-full bg-white shadow-sm transition-transform dark:bg-slate-100',
|
|
120
|
+
thumbSizeClass,
|
|
121
|
+
thumbTranslateClass,
|
|
122
|
+
thumbClassName,
|
|
123
|
+
active ? checkedThumbClassName : uncheckedThumbClassName
|
|
124
|
+
)}
|
|
125
|
+
/>
|
|
126
|
+
</span>
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<button
|
|
131
|
+
type={type}
|
|
132
|
+
role="switch"
|
|
133
|
+
aria-checked={active}
|
|
134
|
+
disabled={disabled}
|
|
135
|
+
onClick={handleClick}
|
|
136
|
+
className={cn(
|
|
137
|
+
'inline-flex items-center gap-2 rounded-full font-semibold transition-all duration-200',
|
|
138
|
+
active ? defaultCheckedClassName : defaultUncheckedClassName,
|
|
139
|
+
className,
|
|
140
|
+
active
|
|
141
|
+
? checkedClassName
|
|
142
|
+
: uncheckedClassName,
|
|
143
|
+
disabled && 'cursor-not-allowed opacity-60'
|
|
144
|
+
)}
|
|
145
|
+
{...props}
|
|
146
|
+
>
|
|
147
|
+
{labelPosition === 'left' ? (
|
|
148
|
+
<>
|
|
149
|
+
{labelNode}
|
|
150
|
+
{trackNode}
|
|
151
|
+
</>
|
|
152
|
+
) : (
|
|
153
|
+
<>
|
|
154
|
+
{trackNode}
|
|
155
|
+
{labelNode}
|
|
156
|
+
</>
|
|
157
|
+
)}
|
|
158
|
+
</button>
|
|
159
|
+
);
|
|
160
|
+
}
|