@windrun-huaiin/third-ui 29.0.2 → 29.0.4

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.
@@ -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.jsxs("button", { type: "button", onClick: () => setPanelMode((prev) => prev === 'info' ? 'test' : 'info'), className: utils.cn('inline-flex items-center gap-2 rounded-full border px-2 py-1 text-[11px] font-semibold shadow-sm transition-all duration-200', panelMode === 'test'
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: [jsxs("button", { type: "button", onClick: () => setPanelMode((prev) => prev === 'info' ? 'test' : 'info'), className: cn('inline-flex items-center gap-2 rounded-full border px-2 py-1 text-[11px] font-semibold shadow-sm transition-all duration-200', panelMode === 'test'
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 || '' }) },
@@ -16,6 +16,7 @@ var button = require('fumadocs-ui/components/ui/button');
16
16
  var i18n = require('fumadocs-ui/contexts/i18n');
17
17
  var headerThemeSwitch = require('./header-theme-switch.js');
18
18
 
19
+ const PrefetchLinkItem = shared.LinkItem;
19
20
  const DEFAULT_DESKTOP_ACTIONS = [
20
21
  'search',
21
22
  'theme',
@@ -163,7 +164,7 @@ const navItemVariants = classVarianceAuthority.cva('[&_svg]:size-4', {
163
164
  },
164
165
  });
165
166
  function NavbarLinkItem(_a) {
166
- var _b;
167
+ var _b, _c;
167
168
  var { item } = _a, props = tslib.__rest(_a, ["item"]);
168
169
  if (item.type === 'custom')
169
170
  return jsxRuntime.jsx("div", Object.assign({}, props, { children: item.children }));
@@ -179,11 +180,11 @@ function NavbarLinkItem(_a) {
179
180
  });
180
181
  return (jsxRuntime.jsxs(navigationMenu.NavigationMenuItem, { children: [jsxRuntime.jsx(navigationMenu.NavigationMenuTrigger, Object.assign({}, props, { className: utils.cn(navItemVariants(), 'rounded-md', props.className), children: item.url ? (jsxRuntime.jsx(Link, { href: item.url, prefetch: (_b = item.prefetch) !== null && _b !== void 0 ? _b : false, external: item.external, children: item.text })) : (item.text) })), jsxRuntime.jsx(navigationMenu.NavigationMenuContent, { className: "grid grid-cols-1 gap-2 p-4 md:grid-cols-2 lg:grid-cols-3", children: children })] }));
181
182
  }
182
- return (jsxRuntime.jsx(navigationMenu.NavigationMenuItem, { children: jsxRuntime.jsx(navigationMenu.NavigationMenuLink, { asChild: true, children: jsxRuntime.jsx(shared.LinkItem, Object.assign({ item: item, "aria-label": item.type === 'icon' ? item.label : undefined }, props, { className: utils.cn(navItemVariants({ variant: item.type }), props.className), children: item.type === 'icon' ? item.icon : item.text })) }) }));
183
+ return (jsxRuntime.jsx(navigationMenu.NavigationMenuItem, { children: jsxRuntime.jsx(navigationMenu.NavigationMenuLink, { asChild: true, children: jsxRuntime.jsx(PrefetchLinkItem, Object.assign({ item: item, prefetch: (_c = item.prefetch) !== null && _c !== void 0 ? _c : false, "aria-label": item.type === 'icon' ? item.label : undefined }, props, { className: utils.cn(navItemVariants({ variant: item.type }), props.className), children: item.type === 'icon' ? item.icon : item.text })) }) }));
183
184
  }
184
185
  const Menu = navigationMenu.NavigationMenuItem;
185
186
  function MenuLinkItem(_a) {
186
- var _b, _c;
187
+ var _b, _c, _d;
187
188
  var { item } = _a, props = tslib.__rest(_a, ["item"]);
188
189
  if (item.type === 'custom')
189
190
  return jsxRuntime.jsx("div", { className: utils.cn('grid', props.className), children: item.children });
@@ -191,7 +192,7 @@ function MenuLinkItem(_a) {
191
192
  const header = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [item.icon, item.text] }));
192
193
  return (jsxRuntime.jsxs("div", { className: utils.cn('mb-4 flex flex-col', props.className), children: [jsxRuntime.jsx("p", { className: "mb-1 text-sm text-fd-muted-foreground", children: item.url ? (jsxRuntime.jsx(navigationMenu.NavigationMenuLink, { asChild: true, children: jsxRuntime.jsx(Link, { href: item.url, prefetch: (_b = item.prefetch) !== null && _b !== void 0 ? _b : false, external: item.external, children: header }) })) : (header) }), item.items.map((child, i) => (jsxRuntime.jsx(MenuLinkItem, { item: child }, i)))] }));
193
194
  }
194
- return (jsxRuntime.jsx(navigationMenu.NavigationMenuLink, { asChild: true, children: jsxRuntime.jsxs(shared.LinkItem, { item: item, className: utils.cn({
195
+ return (jsxRuntime.jsx(navigationMenu.NavigationMenuLink, { asChild: true, children: jsxRuntime.jsxs(PrefetchLinkItem, { item: item, prefetch: (_c = item.prefetch) !== null && _c !== void 0 ? _c : false, className: utils.cn({
195
196
  main: 'inline-flex items-center gap-2 py-1.5 transition-colors hover:text-fd-popover-foreground/50 data-[active=true]:font-medium data-[active=true]:text-fd-primary [&_svg]:size-4',
196
197
  icon: button.buttonVariants({
197
198
  size: 'icon',
@@ -201,7 +202,7 @@ function MenuLinkItem(_a) {
201
202
  color: 'secondary',
202
203
  className: 'gap-1.5 [&_svg]:size-4',
203
204
  }),
204
- }[(_c = item.type) !== null && _c !== void 0 ? _c : 'main'], props.className), "aria-label": item.type === 'icon' ? item.label : undefined, children: [item.icon, item.type === 'icon' ? undefined : item.text] }) }));
205
+ }[(_d = item.type) !== null && _d !== void 0 ? _d : 'main'], props.className), "aria-label": item.type === 'icon' ? item.label : undefined, children: [item.icon, item.type === 'icon' ? undefined : item.text] }) }));
205
206
  }
206
207
  function MenuTrigger(_a) {
207
208
  var { enableHover = false } = _a, props = tslib.__rest(_a, ["enableHover"]);
@@ -14,6 +14,7 @@ import { buttonVariants } from 'fumadocs-ui/components/ui/button';
14
14
  import { useI18n } from 'fumadocs-ui/contexts/i18n';
15
15
  import { HeaderThemeSwitch } from './header-theme-switch.mjs';
16
16
 
17
+ const PrefetchLinkItem = LinkItem;
17
18
  const DEFAULT_DESKTOP_ACTIONS = [
18
19
  'search',
19
20
  'theme',
@@ -161,7 +162,7 @@ const navItemVariants = cva('[&_svg]:size-4', {
161
162
  },
162
163
  });
163
164
  function NavbarLinkItem(_a) {
164
- var _b;
165
+ var _b, _c;
165
166
  var { item } = _a, props = __rest(_a, ["item"]);
166
167
  if (item.type === 'custom')
167
168
  return jsx("div", Object.assign({}, props, { children: item.children }));
@@ -177,11 +178,11 @@ function NavbarLinkItem(_a) {
177
178
  });
178
179
  return (jsxs(NavigationMenuItem, { children: [jsx(NavigationMenuTrigger, Object.assign({}, props, { className: cn(navItemVariants(), 'rounded-md', props.className), children: item.url ? (jsx(Link, { href: item.url, prefetch: (_b = item.prefetch) !== null && _b !== void 0 ? _b : false, external: item.external, children: item.text })) : (item.text) })), jsx(NavigationMenuContent, { className: "grid grid-cols-1 gap-2 p-4 md:grid-cols-2 lg:grid-cols-3", children: children })] }));
179
180
  }
180
- return (jsx(NavigationMenuItem, { children: jsx(NavigationMenuLink, { asChild: true, children: jsx(LinkItem, Object.assign({ item: item, "aria-label": item.type === 'icon' ? item.label : undefined }, props, { className: cn(navItemVariants({ variant: item.type }), props.className), children: item.type === 'icon' ? item.icon : item.text })) }) }));
181
+ return (jsx(NavigationMenuItem, { children: jsx(NavigationMenuLink, { asChild: true, children: jsx(PrefetchLinkItem, Object.assign({ item: item, prefetch: (_c = item.prefetch) !== null && _c !== void 0 ? _c : false, "aria-label": item.type === 'icon' ? item.label : undefined }, props, { className: cn(navItemVariants({ variant: item.type }), props.className), children: item.type === 'icon' ? item.icon : item.text })) }) }));
181
182
  }
182
183
  const Menu = NavigationMenuItem;
183
184
  function MenuLinkItem(_a) {
184
- var _b, _c;
185
+ var _b, _c, _d;
185
186
  var { item } = _a, props = __rest(_a, ["item"]);
186
187
  if (item.type === 'custom')
187
188
  return jsx("div", { className: cn('grid', props.className), children: item.children });
@@ -189,7 +190,7 @@ function MenuLinkItem(_a) {
189
190
  const header = (jsxs(Fragment, { children: [item.icon, item.text] }));
190
191
  return (jsxs("div", { className: cn('mb-4 flex flex-col', props.className), children: [jsx("p", { className: "mb-1 text-sm text-fd-muted-foreground", children: item.url ? (jsx(NavigationMenuLink, { asChild: true, children: jsx(Link, { href: item.url, prefetch: (_b = item.prefetch) !== null && _b !== void 0 ? _b : false, external: item.external, children: header }) })) : (header) }), item.items.map((child, i) => (jsx(MenuLinkItem, { item: child }, i)))] }));
191
192
  }
192
- return (jsx(NavigationMenuLink, { asChild: true, children: jsxs(LinkItem, { item: item, className: cn({
193
+ return (jsx(NavigationMenuLink, { asChild: true, children: jsxs(PrefetchLinkItem, { item: item, prefetch: (_c = item.prefetch) !== null && _c !== void 0 ? _c : false, className: cn({
193
194
  main: 'inline-flex items-center gap-2 py-1.5 transition-colors hover:text-fd-popover-foreground/50 data-[active=true]:font-medium data-[active=true]:text-fd-primary [&_svg]:size-4',
194
195
  icon: buttonVariants({
195
196
  size: 'icon',
@@ -199,7 +200,7 @@ function MenuLinkItem(_a) {
199
200
  color: 'secondary',
200
201
  className: 'gap-1.5 [&_svg]:size-4',
201
202
  }),
202
- }[(_c = item.type) !== null && _c !== void 0 ? _c : 'main'], props.className), "aria-label": item.type === 'icon' ? item.label : undefined, children: [item.icon, item.type === 'icon' ? undefined : item.text] }) }));
203
+ }[(_d = item.type) !== null && _d !== void 0 ? _d : 'main'], props.className), "aria-label": item.type === 'icon' ? item.label : undefined, children: [item.icon, item.type === 'icon' ? undefined : item.text] }) }));
203
204
  }
204
205
  function MenuTrigger(_a) {
205
206
  var { enableHover = false } = _a, props = __rest(_a, ["enableHover"]);
@@ -2,3 +2,4 @@ export * from './ads-alert-dialog';
2
2
  export * from './confirm-dialog';
3
3
  export * from './high-priority-confirm-dialog';
4
4
  export * from './info-dialog';
5
+ export * from './undoable-confirm-dialog';
@@ -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 };
@@ -1,3 +1,4 @@
1
1
  export * from './gradient-button';
2
2
  export * from './x-button';
3
+ export * from './x-switch-button';
3
4
  export * from './x-toggle-button';
@@ -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;
@@ -1,4 +1,5 @@
1
1
  "use client";
2
2
  export { GradientButton } from './gradient-button.mjs';
3
3
  export { XButton } from './x-button.mjs';
4
+ export { XSwitchButton } from './x-switch-button.mjs';
4
5
  export { XToggleButton } from './x-toggle-button.mjs';
@@ -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 };
@@ -23,7 +23,7 @@ function Footer(_a) {
23
23
  clickToCopyText: tIntl.safeT(tFooter, 'clickToCopy', 'Click to copy'),
24
24
  copiedText: tIntl.safeT(tFooter, 'copied', 'Copied!'),
25
25
  };
26
- return (jsxRuntime.jsxs("div", { className: "mb-10 w-full mx-auto", children: [jsxRuntime.jsx("div", { className: utils.cn("w-full border-current border-t", lib.themeIconColor) }), jsxRuntime.jsx("footer", { children: jsxRuntime.jsxs("div", { className: "w-full flex flex-col items-center justify-center px-4 py-8 space-y-3", children: [jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-center gap-x-2 gap-y-2 text-xs sm:text-sm sm:gap-x-6", children: [jsxRuntime.jsxs(Link, { href: utils.getAsNeededLocalizedUrl(locale, "/legal/terms", localePrefixAsNeeded, defaultLocale), className: "flex items-center space-x-1 hover:underline", children: [jsxRuntime.jsx(icons.ReceiptTextIcon, { className: "h-3.5 w-3.5" }), jsxRuntime.jsx("span", { children: data.terms })] }), jsxRuntime.jsxs(Link, { href: utils.getAsNeededLocalizedUrl(locale, "/legal/privacy", localePrefixAsNeeded, defaultLocale), className: "flex items-center space-x-1 hover:underline", children: [jsxRuntime.jsx(icons.ShieldUserIcon, { className: "h-3.5 w-3.5" }), jsxRuntime.jsx("span", { children: data.privacy })] }), jsxRuntime.jsxs(footerEmail.FooterEmail, { email: data.email, clickToCopyText: data.clickToCopyText, copiedText: data.copiedText, children: [jsxRuntime.jsx(icons.MailIcon, { className: "h-3.5 w-3.5" }), jsxRuntime.jsx("span", { children: data.contactUs })] })] }), jsxRuntime.jsx("div", { className: "text-xs sm:text-sm text-center", children: jsxRuntime.jsx("span", { children: data.copyright }) })] }) })] }));
26
+ return (jsxRuntime.jsxs("div", { className: "mb-10 w-full mx-auto", children: [jsxRuntime.jsx("div", { className: utils.cn("w-full border-current border-t", lib.themeIconColor) }), jsxRuntime.jsx("footer", { children: jsxRuntime.jsxs("div", { className: "w-full flex flex-col items-center justify-center px-4 py-8 space-y-3", children: [jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center justify-center gap-x-2 gap-y-2 text-xs sm:text-sm sm:gap-x-6", children: [jsxRuntime.jsxs(Link, { prefetch: false, href: utils.getAsNeededLocalizedUrl(locale, "/legal/terms", localePrefixAsNeeded, defaultLocale), className: "flex items-center space-x-1 hover:underline", children: [jsxRuntime.jsx(icons.ReceiptTextIcon, { className: "h-3.5 w-3.5" }), jsxRuntime.jsx("span", { children: data.terms })] }), jsxRuntime.jsxs(Link, { prefetch: false, href: utils.getAsNeededLocalizedUrl(locale, "/legal/privacy", localePrefixAsNeeded, defaultLocale), className: "flex items-center space-x-1 hover:underline", children: [jsxRuntime.jsx(icons.ShieldUserIcon, { className: "h-3.5 w-3.5" }), jsxRuntime.jsx("span", { children: data.privacy })] }), jsxRuntime.jsxs(footerEmail.FooterEmail, { email: data.email, clickToCopyText: data.clickToCopyText, copiedText: data.copiedText, children: [jsxRuntime.jsx(icons.MailIcon, { className: "h-3.5 w-3.5" }), jsxRuntime.jsx("span", { children: data.contactUs })] })] }), jsxRuntime.jsx("div", { className: "text-xs sm:text-sm text-center", children: jsxRuntime.jsx("span", { children: data.copyright }) })] }) })] }));
27
27
  });
28
28
  }
29
29
 
@@ -21,7 +21,7 @@ function Footer(_a) {
21
21
  clickToCopyText: safeT(tFooter, 'clickToCopy', 'Click to copy'),
22
22
  copiedText: safeT(tFooter, 'copied', 'Copied!'),
23
23
  };
24
- return (jsxs("div", { className: "mb-10 w-full mx-auto", children: [jsx("div", { className: cn("w-full border-current border-t", themeIconColor) }), jsx("footer", { children: jsxs("div", { className: "w-full flex flex-col items-center justify-center px-4 py-8 space-y-3", children: [jsxs("div", { className: "flex flex-wrap items-center justify-center gap-x-2 gap-y-2 text-xs sm:text-sm sm:gap-x-6", children: [jsxs(Link, { href: getAsNeededLocalizedUrl(locale, "/legal/terms", localePrefixAsNeeded, defaultLocale), className: "flex items-center space-x-1 hover:underline", children: [jsx(ReceiptTextIcon, { className: "h-3.5 w-3.5" }), jsx("span", { children: data.terms })] }), jsxs(Link, { href: getAsNeededLocalizedUrl(locale, "/legal/privacy", localePrefixAsNeeded, defaultLocale), className: "flex items-center space-x-1 hover:underline", children: [jsx(ShieldUserIcon, { className: "h-3.5 w-3.5" }), jsx("span", { children: data.privacy })] }), jsxs(FooterEmail, { email: data.email, clickToCopyText: data.clickToCopyText, copiedText: data.copiedText, children: [jsx(MailIcon, { className: "h-3.5 w-3.5" }), jsx("span", { children: data.contactUs })] })] }), jsx("div", { className: "text-xs sm:text-sm text-center", children: jsx("span", { children: data.copyright }) })] }) })] }));
24
+ return (jsxs("div", { className: "mb-10 w-full mx-auto", children: [jsx("div", { className: cn("w-full border-current border-t", themeIconColor) }), jsx("footer", { children: jsxs("div", { className: "w-full flex flex-col items-center justify-center px-4 py-8 space-y-3", children: [jsxs("div", { className: "flex flex-wrap items-center justify-center gap-x-2 gap-y-2 text-xs sm:text-sm sm:gap-x-6", children: [jsxs(Link, { prefetch: false, href: getAsNeededLocalizedUrl(locale, "/legal/terms", localePrefixAsNeeded, defaultLocale), className: "flex items-center space-x-1 hover:underline", children: [jsx(ReceiptTextIcon, { className: "h-3.5 w-3.5" }), jsx("span", { children: data.terms })] }), jsxs(Link, { prefetch: false, href: getAsNeededLocalizedUrl(locale, "/legal/privacy", localePrefixAsNeeded, defaultLocale), className: "flex items-center space-x-1 hover:underline", children: [jsx(ShieldUserIcon, { className: "h-3.5 w-3.5" }), jsx("span", { children: data.privacy })] }), jsxs(FooterEmail, { email: data.email, clickToCopyText: data.clickToCopyText, copiedText: data.copiedText, children: [jsx(MailIcon, { className: "h-3.5 w-3.5" }), jsx("span", { children: data.contactUs })] })] }), jsx("div", { className: "text-xs sm:text-sm text-center", children: jsx("span", { children: data.copyright }) })] }) })] }));
25
25
  });
26
26
  }
27
27
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windrun-huaiin/third-ui",
3
- "version": "29.0.2",
3
+ "version": "29.0.4",
4
4
  "description": "Third-party integrated UI components for windrun-huaiin projects",
5
5
  "exports": {
6
6
  "./clerk": {
@@ -227,9 +227,9 @@
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.0",
231
- "@windrun-huaiin/lib": "^29.0.0",
232
- "@windrun-huaiin/contracts": "^29.0.0"
230
+ "@windrun-huaiin/base-ui": "^29.0.1",
231
+ "@windrun-huaiin/contracts": "^29.0.0",
232
+ "@windrun-huaiin/lib": "^29.0.0"
233
233
  },
234
234
  "peerDependencies": {
235
235
  "clsx": "^2.1.1",
@@ -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
- <button
440
+ <XSwitchButton
440
441
  type="button"
441
- onClick={() => setPanelMode((prev) => prev === 'info' ? 'test' : 'info')}
442
- className={cn(
443
- 'inline-flex items-center gap-2 rounded-full border px-2 py-1 text-[11px] font-semibold shadow-sm transition-all duration-200',
444
- panelMode === 'test'
445
- ? cn('border-transparent text-white', themeButtonGradientClass, themeButtonGradientHoverClass)
446
- : themedGhostButtonClass
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}
@@ -43,6 +43,10 @@ export type NavbarCSSVars = CSSProperties & {
43
43
  '--fd-nav-max-width'?: string;
44
44
  };
45
45
 
46
+ const PrefetchLinkItem = LinkItem as (
47
+ props: ComponentProps<typeof LinkItem> & { prefetch?: boolean },
48
+ ) => ReactNode;
49
+
46
50
  export interface CustomHomeHeaderProps extends HomeLayoutProps {
47
51
  /**
48
52
  * Banner height in rem units
@@ -527,8 +531,9 @@ function NavbarLinkItem({
527
531
  return (
528
532
  <NavigationMenuItem>
529
533
  <NavigationMenuLink asChild>
530
- <LinkItem
534
+ <PrefetchLinkItem
531
535
  item={item}
536
+ prefetch={item.prefetch ?? false}
532
537
  aria-label={item.type === 'icon' ? item.label : undefined}
533
538
  {...props}
534
539
  className={cn(
@@ -537,7 +542,7 @@ function NavbarLinkItem({
537
542
  )}
538
543
  >
539
544
  {item.type === 'icon' ? item.icon : item.text}
540
- </LinkItem>
545
+ </PrefetchLinkItem>
541
546
  </NavigationMenuLink>
542
547
  </NavigationMenuItem>
543
548
  );
@@ -585,8 +590,9 @@ function MenuLinkItem({
585
590
 
586
591
  return (
587
592
  <NavigationMenuLink asChild>
588
- <LinkItem
593
+ <PrefetchLinkItem
589
594
  item={item}
595
+ prefetch={item.prefetch ?? false}
590
596
  className={cn(
591
597
  {
592
598
  main: 'inline-flex items-center gap-2 py-1.5 transition-colors hover:text-fd-popover-foreground/50 data-[active=true]:font-medium data-[active=true]:text-fd-primary [&_svg]:size-4',
@@ -605,7 +611,7 @@ function MenuLinkItem({
605
611
  >
606
612
  {item.icon}
607
613
  {item.type === 'icon' ? undefined : item.text}
608
- </LinkItem>
614
+ </PrefetchLinkItem>
609
615
  </NavigationMenuLink>
610
616
  );
611
617
  }
@@ -4,4 +4,4 @@ export * from './ads-alert-dialog';
4
4
  export * from './confirm-dialog';
5
5
  export * from './high-priority-confirm-dialog';
6
6
  export * from './info-dialog';
7
-
7
+ export * from './undoable-confirm-dialog';
@@ -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
+ }
@@ -2,4 +2,5 @@
2
2
 
3
3
  export * from './gradient-button';
4
4
  export * from './x-button';
5
+ export * from './x-switch-button';
5
6
  export * from './x-toggle-button';
@@ -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
+ }
@@ -45,11 +45,11 @@ export async function Footer({ locale, localePrefixAsNeeded = true, defaultLocal
45
45
  <footer>
46
46
  <div className="w-full flex flex-col items-center justify-center px-4 py-8 space-y-3">
47
47
  <div className="flex flex-wrap items-center justify-center gap-x-2 gap-y-2 text-xs sm:text-sm sm:gap-x-6">
48
- <Link href={getAsNeededLocalizedUrl(locale, "/legal/terms", localePrefixAsNeeded, defaultLocale)} className="flex items-center space-x-1 hover:underline">
48
+ <Link prefetch={false} href={getAsNeededLocalizedUrl(locale, "/legal/terms", localePrefixAsNeeded, defaultLocale)} className="flex items-center space-x-1 hover:underline">
49
49
  <ReceiptTextIcon className="h-3.5 w-3.5"/>
50
50
  <span>{data.terms}</span>
51
51
  </Link>
52
- <Link href={getAsNeededLocalizedUrl(locale, "/legal/privacy", localePrefixAsNeeded, defaultLocale)} className="flex items-center space-x-1 hover:underline">
52
+ <Link prefetch={false} href={getAsNeededLocalizedUrl(locale, "/legal/privacy", localePrefixAsNeeded, defaultLocale)} className="flex items-center space-x-1 hover:underline">
53
53
  <ShieldUserIcon className="h-3.5 w-3.5"/>
54
54
  <span>{data.privacy}</span>
55
55
  </Link>