From bc5ed496aa27d938d2e2d368b555df37da072d22 Mon Sep 17 00:00:00 2001
From: hzm <934585316@qq.com>
Date: Sun, 15 Mar 2026 00:03:21 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=A4=A7=E7=9B=98?=
=?UTF-8?q?=E6=8C=87=E6=95=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/api/fund.js | 85 +++++
app/components/MarketIndexAccordion.jsx | 481 ++++++++++++++++++++++++
app/components/MarketSettingModal.jsx | 409 ++++++++++++++++++++
app/globals.css | 15 +-
app/page.jsx | 17 +-
components/ui/accordion.jsx | 64 ++++
6 files changed, 1065 insertions(+), 6 deletions(-)
create mode 100644 app/components/MarketIndexAccordion.jsx
create mode 100644 app/components/MarketSettingModal.jsx
create mode 100644 components/ui/accordion.jsx
diff --git a/app/api/fund.js b/app/api/fund.js
index 7c75713..218bf90 100644
--- a/app/api/fund.js
+++ b/app/api/fund.js
@@ -513,6 +513,91 @@ export const fetchShanghaiIndexDate = async () => {
});
};
+/** 大盘指数项:name, code, price, change, changePercent
+ * 同时用于:
+ * - qt.gtimg.cn 实时快照(code 用于 q= 参数,varKey 为全局变量名)
+ * - 分时 mini 图(code 传给 minute/query,当不支持分时时会自动回退占位折线)
+ *
+ * 参照产品图:覆盖主要 A 股宽基 + 创业/科创 + 部分海外与港股指数。
+ */
+const MARKET_INDEX_KEYS = [
+ // 行 1:上证 / 深证
+ { code: 'sh000001', varKey: 'v_sh000001', name: '上证指数' },
+ { code: 'sh000016', varKey: 'v_sh000016', name: '上证50' },
+ { code: 'sz399001', varKey: 'v_sz399001', name: '深证成指' },
+ { code: 'sz399330', varKey: 'v_sz399330', name: '深证100' },
+
+ // 行 2:北证 / 沪深300 / 创业板
+ { code: 'bj899050', varKey: 'v_bj899050', name: '北证50' },
+ { code: 'sh000300', varKey: 'v_sh000300', name: '沪深300' },
+ { code: 'sz399006', varKey: 'v_sz399006', name: '创业板指' },
+ { code: 'sz399102', varKey: 'v_sz399102', name: '创业板综' },
+
+ // 行 3:创业板 50 / 科创
+ { code: 'sz399673', varKey: 'v_sz399673', name: '创业板50' },
+ { code: 'sh000688', varKey: 'v_sh000688', name: '科创50' },
+ { code: 'sz399005', varKey: 'v_sz399005', name: '中小100' },
+
+ // 行 4:中证系列
+ { code: 'sh000905', varKey: 'v_sh000905', name: '中证500' },
+ { code: 'sh000906', varKey: 'v_sh000906', name: '中证800' },
+ { code: 'sh000852', varKey: 'v_sh000852', name: '中证1000' },
+ { code: 'sh000903', varKey: 'v_sh000903', name: '中证A100' },
+
+ // 行 5:等权 / 国证 / 纳指
+ { code: 'sh000932', varKey: 'v_sh000932', name: '500等权' },
+ { code: 'sz399303', varKey: 'v_sz399303', name: '国证2000' },
+ { code: 'usIXIC', varKey: 'v_usIXIC', name: '纳斯达克' },
+ { code: 'usNDX', varKey: 'v_usNDX', name: '纳斯达克100' },
+
+ // 行 6:美股三大 + 恒生
+ { code: 'usINX', varKey: 'v_usINX', name: '标普500' },
+ { code: 'usDJI', varKey: 'v_usDJI', name: '道琼斯' },
+ { code: 'hkHSI', varKey: 'v_hkHSI', name: '恒生指数' },
+ { code: 'hkHSTECH', varKey: 'v_hkHSTECH', name: '恒生科技指数' },
+];
+
+function parseIndexRaw(data) {
+ if (!data || typeof data !== 'string') return null;
+ const parts = data.split('~');
+ if (parts.length < 33) return null;
+ const name = parts[1] || '';
+ const price = parseFloat(parts[3], 10);
+ const change = parseFloat(parts[31], 10);
+ const changePercent = parseFloat(parts[32], 10);
+ if (Number.isNaN(price)) return null;
+ return {
+ name,
+ price: Number.isFinite(price) ? price : 0,
+ change: Number.isFinite(change) ? change : 0,
+ changePercent: Number.isFinite(changePercent) ? changePercent : 0,
+ };
+}
+
+export const fetchMarketIndices = async () => {
+ if (typeof window === 'undefined' || typeof document === 'undefined') return [];
+ return new Promise((resolve, reject) => {
+ const script = document.createElement('script');
+ const codes = MARKET_INDEX_KEYS.map((item) => item.code).join(',');
+ script.src = `https://qt.gtimg.cn/q=${codes}&_t=${Date.now()}`;
+ script.onload = () => {
+ const list = MARKET_INDEX_KEYS.map(({ name: defaultName, varKey }) => {
+ const raw = window[varKey];
+ const parsed = parseIndexRaw(raw);
+ if (!parsed) return { name: defaultName, code: '', price: 0, change: 0, changePercent: 0 };
+ return { ...parsed, code: varKey.replace('v_', '') };
+ });
+ if (document.body.contains(script)) document.body.removeChild(script);
+ resolve(list);
+ };
+ script.onerror = () => {
+ if (document.body.contains(script)) document.body.removeChild(script);
+ reject(new Error('指数数据加载失败'));
+ };
+ document.body.appendChild(script);
+ });
+};
+
export const fetchLatestRelease = async () => {
const url = process.env.NEXT_PUBLIC_GITHUB_LATEST_RELEASE_URL;
if (!url) return null;
diff --git a/app/components/MarketIndexAccordion.jsx b/app/components/MarketIndexAccordion.jsx
new file mode 100644
index 0000000..10af6e6
--- /dev/null
+++ b/app/components/MarketIndexAccordion.jsx
@@ -0,0 +1,481 @@
+'use client';
+
+import { useEffect, useState, useMemo, useRef } from 'react';
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from '@/components/ui/accordion';
+import { fetchMarketIndices } from '@/app/api/fund';
+import { ChevronRightIcon } from 'lucide-react';
+import { SettingsIcon } from './Icons';
+import { cn } from '@/lib/utils';
+import MarketSettingModal from './MarketSettingModal';
+
+/** 简单伪随机,用于稳定迷你图形状 */
+function seeded(seed) {
+ return () => {
+ seed = (seed * 9301 + 49297) % 233280;
+ return seed / 233280;
+ };
+}
+
+/** 迷你走势:优先展示当日分时数据,失败时退回占位折线 */
+function MiniTrendLine({ changePercent, code, className }) {
+ const isDown = changePercent <= 0;
+ const width = 80;
+ const height = 28;
+ const pad = 3;
+ const innerH = height - 2 * pad;
+ const innerW = width - 2 * pad;
+
+ // 占位伪走势(无真实历史数据)
+ const fallbackPath = useMemo(() => {
+ const points = 12;
+ const rnd = seeded(Math.abs(Math.floor(changePercent * 100)) + 1);
+ const arr = Array.from({ length: points }, (_, i) => {
+ const t = i / (points - 1);
+ const x = pad + t * innerW;
+ const y = isDown
+ ? pad + innerH * (1 - t * 0.6) - (rnd() * 4 - 2)
+ : pad + innerH * (0.4 + t * 0.6) + (rnd() * 4 - 2);
+ return [x, Math.max(pad, Math.min(height - pad, y))];
+ });
+ return arr.map(([x, y], i) => `${i === 0 ? 'M' : 'L'} ${x} ${y}`).join(' ');
+ }, [changePercent, isDown, innerH, innerW, pad, height]);
+
+ // 当日分时真实走势 path
+ const [realPath, setRealPath] = useState(null);
+
+ useEffect(() => {
+ if (!code || typeof window === 'undefined' || typeof document === 'undefined') {
+ setRealPath(null);
+ return;
+ }
+
+ let cancelled = false;
+ const varName = `min_data_${code}`;
+ const url = `https://web.ifzq.gtimg.cn/appstock/app/minute/query?_var=${varName}&code=${code}&_=${Date.now()}`;
+
+ const script = document.createElement('script');
+ script.src = url;
+ script.async = true;
+
+ const cleanup = () => {
+ if (document.body && document.body.contains(script)) {
+ document.body.removeChild(script);
+ }
+ try {
+ if (window[varName]) {
+ delete window[varName];
+ }
+ } catch (e) {
+ // ignore
+ }
+ };
+
+ script.onload = () => {
+ if (cancelled) {
+ cleanup();
+ return;
+ }
+ try {
+ const raw = window[varName];
+ const series =
+ raw &&
+ raw.data &&
+ raw.data[code] &&
+ raw.data[code].data &&
+ Array.isArray(raw.data[code].data.data)
+ ? raw.data[code].data.data
+ : null;
+ if (!series || !series.length) {
+ setRealPath(null);
+ return;
+ }
+
+ // 解析 "HHMM price volume amount" 行,只关心 price
+ const points = series
+ .map((row) => {
+ const parts = String(row).split(' ');
+ const price = parseFloat(parts[1]);
+ if (!Number.isFinite(price)) return null;
+ return { price };
+ })
+ .filter(Boolean);
+
+ if (!points.length) {
+ setRealPath(null);
+ return;
+ }
+
+ const minP = points.reduce((m, p) => (p.price < m ? p.price : m), points[0].price);
+ const maxP = points.reduce((m, p) => (p.price > m ? p.price : m), points[0].price);
+ const span = maxP - minP || 1;
+
+ const n = points.length;
+ const pathPoints = points.map((p, idx) => {
+ const t = n > 1 ? idx / (n - 1) : 0;
+ const x = pad + t * innerW;
+ const norm = (p.price - minP) / span;
+ const y = pad + (1 - norm) * innerH;
+ return [x, Math.max(pad, Math.min(height - pad, y))];
+ });
+
+ const d = pathPoints
+ .map(([x, y], i) => `${i === 0 ? 'M' : 'L'} ${x} ${y}`)
+ .join(' ');
+ setRealPath(d);
+ } finally {
+ cleanup();
+ }
+ };
+
+ script.onerror = () => {
+ if (!cancelled) {
+ setRealPath(null);
+ }
+ cleanup();
+ };
+
+ document.body.appendChild(script);
+
+ return () => {
+ cancelled = true;
+ cleanup();
+ };
+ }, [code, height, innerH, innerW, pad]);
+
+ const d = realPath || fallbackPath;
+ return (
+
+ );
+}
+
+function IndexCard({ item }) {
+ const isUp = item.change >= 0;
+ const colorClass = isUp ? 'text-[var(--danger)]' : 'text-[var(--success)]';
+ return (
+
+
{item.name}
+
+ {item.price.toFixed(2)}
+
+
+ {(item.change >= 0 ? '+' : '') + item.change.toFixed(2)}{' '}
+ {(item.changePercent >= 0 ? '+' : '') + item.changePercent.toFixed(2)}%
+
+
+
+
+
+ );
+}
+
+// 默认展示:上证指数、深证成指、创业板指
+const DEFAULT_SELECTED_CODES = ['sh000001', 'sz399001', 'sz399006'];
+
+export default function MarketIndexAccordion({
+ navbarHeight = 0,
+ onHeightChange,
+ isMobile,
+ onCustomSettingsChange,
+}) {
+ const [indices, setIndices] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [openValue, setOpenValue] = useState('');
+ const [selectedCodes, setSelectedCodes] = useState([]);
+ const [settingOpen, setSettingOpen] = useState(false);
+ const [tickerIndex, setTickerIndex] = useState(0);
+ const rootRef = useRef(null);
+
+ useEffect(() => {
+ const el = rootRef.current;
+ if (!el || typeof onHeightChange !== 'function') return;
+ const ro = new ResizeObserver((entries) => {
+ const entry = entries[0];
+ if (entry) onHeightChange(entry.contentRect.height);
+ });
+ ro.observe(el);
+ onHeightChange(el.getBoundingClientRect().height);
+ return () => {
+ ro.disconnect();
+ onHeightChange(0);
+ };
+ }, [onHeightChange, loading, indices.length]);
+
+ useEffect(() => {
+ let cancelled = false;
+ fetchMarketIndices()
+ .then((data) => {
+ if (!cancelled) setIndices(Array.isArray(data) ? data : []);
+ })
+ .catch(() => {
+ if (!cancelled) setIndices([]);
+ })
+ .finally(() => {
+ if (!cancelled) setLoading(false);
+ });
+ return () => { cancelled = true; };
+ }, []);
+
+ // 初始化选中指数(本地偏好 > 默认集合)
+ useEffect(() => {
+ if (!indices.length || typeof window === 'undefined') return;
+ try {
+ const stored = window.localStorage.getItem('marketIndexSelected');
+ const availableCodes = new Set(indices.map((it) => it.code));
+ if (stored) {
+ const parsed = JSON.parse(stored);
+ if (Array.isArray(parsed)) {
+ const filtered = parsed.filter((c) => availableCodes.has(c));
+ if (filtered.length) {
+ setSelectedCodes(filtered);
+ return;
+ }
+ }
+ }
+ const defaults = DEFAULT_SELECTED_CODES.filter((c) => availableCodes.has(c));
+ setSelectedCodes(defaults.length ? defaults : indices.map((it) => it.code).slice(0, 3));
+ } catch {
+ setSelectedCodes(indices.map((it) => it.code).slice(0, 3));
+ }
+ }, [indices]);
+
+ // 持久化用户选择
+ useEffect(() => {
+ if (typeof window === 'undefined') return;
+ if (!selectedCodes.length) return;
+ try {
+ // 本地首选 key:独立存储,便于快速读取
+ window.localStorage.setItem('marketIndexSelected', JSON.stringify(selectedCodes));
+
+ // 同步到 customSettings,便于云端同步
+ const raw = window.localStorage.getItem('customSettings');
+ const parsed = raw ? JSON.parse(raw) : {};
+ const next = parsed && typeof parsed === 'object' ? { ...parsed, marketIndexSelected: selectedCodes } : { marketIndexSelected: selectedCodes };
+ window.localStorage.setItem('customSettings', JSON.stringify(next));
+ onCustomSettingsChange?.();
+ } catch {
+ // ignore
+ }
+ }, [selectedCodes]);
+ // 用户已选择的指数列表(按 selectedCodes 顺序)
+ const visibleIndices = selectedCodes.length
+ ? selectedCodes
+ .map((code) => indices.find((it) => it.code === code))
+ .filter(Boolean)
+ : indices;
+
+ // 重置 tickerIndex 确保索引合法
+ useEffect(() => {
+ if (tickerIndex >= visibleIndices.length) {
+ setTickerIndex(0);
+ }
+ }, [visibleIndices.length, tickerIndex]);
+
+ // 收起状态下轮播展示指数
+ useEffect(() => {
+ if (!visibleIndices.length) return;
+ if (openValue === 'indices') return;
+ if (visibleIndices.length <= 1) return;
+ const timer = setInterval(() => {
+ setTickerIndex((prev) => (prev + 1) % visibleIndices.length);
+ }, 4000);
+ return () => clearInterval(timer);
+ }, [visibleIndices.length, openValue]);
+
+ const current =
+ visibleIndices.length === 0
+ ? null
+ : visibleIndices[openValue === 'indices' ? 0 : tickerIndex];
+
+ const isUp = current ? current.change >= 0 : false;
+ const colorClass = isUp ? 'text-[var(--danger)]' : 'text-[var(--success)]';
+
+ const topMargin = Number(navbarHeight) || 0;
+ const stickyStyle = {
+ marginTop: topMargin,
+ position: 'sticky',
+ top: topMargin,
+ zIndex: 10,
+ width: 'calc(100% + 24px)',
+ marginLeft: -12,
+ };
+
+ if (loading && indices.length === 0) {
+ return (
+
+ 加载大盘指数…
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ {current ? (
+
+
+
+ {current.name}
+
+
+ {current.price.toFixed(2)}
+
+
+ {(current.change >= 0 ? '+' : '') + current.change.toFixed(2)}
+
+
+ {(current.changePercent >= 0 ? '+' : '') + current.changePercent.toFixed(2)}%
+
+
+
+ ) : (
+
暂无指数数据
+ )}
+
+
+
{
+ e.stopPropagation();
+ setSettingOpen(true);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ e.stopPropagation();
+ setSettingOpen(true);
+ }
+ }}
+ aria-label="指数个性化设置"
+ >
+
+
+
+
+
+
+
+ {visibleIndices.map((item, i) => (
+
+
+
+ ))}
+
+
+
+
+
setSettingOpen(false)}
+ isMobile={isMobile}
+ indices={indices}
+ selectedCodes={selectedCodes}
+ onChangeSelected={setSelectedCodes}
+ onResetDefault={() => {
+ const availableCodes = new Set(indices.map((it) => it.code));
+ const defaults = DEFAULT_SELECTED_CODES.filter((c) => availableCodes.has(c));
+ setSelectedCodes(defaults.length ? defaults : indices.map((it) => it.code).slice(0, 3));
+ }}
+ />
+
+ );
+}
diff --git a/app/components/MarketSettingModal.jsx b/app/components/MarketSettingModal.jsx
new file mode 100644
index 0000000..893f0e1
--- /dev/null
+++ b/app/components/MarketSettingModal.jsx
@@ -0,0 +1,409 @@
+"use client";
+
+import { useMemo, useState } from "react";
+import { AnimatePresence, Reorder } from "framer-motion";
+import {
+ Drawer,
+ DrawerContent,
+ DrawerHeader,
+ DrawerTitle,
+ DrawerClose,
+} from "@/components/ui/drawer";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogClose,
+} from "@/components/ui/dialog";
+import { CloseIcon, MinusIcon, ResetIcon, SettingsIcon } from "./Icons";
+import ConfirmModal from "./ConfirmModal";
+import { cn } from "@/lib/utils";
+
+/**
+ * 指数个性化设置弹框
+ *
+ * - 移动端:使用 Drawer(自底向上抽屉)
+ * - PC 端:使用 Dialog(居中弹窗)
+ *
+ * @param {Object} props
+ * @param {boolean} props.open - 是否打开
+ * @param {() => void} props.onClose - 关闭回调
+ * @param {boolean} props.isMobile - 是否为移动端(由上层传入)
+ * @param {Array<{code:string,name:string,price:number,change:number,changePercent:number}>} props.indices - 当前可用的大盘指数列表
+ * @param {string[]} props.selectedCodes - 已选中的指数 code,决定展示顺序
+ * @param {(codes: string[]) => void} props.onChangeSelected - 更新选中指数集合
+ * @param {() => void} props.onResetDefault - 恢复默认选中集合
+ */
+export default function MarketSettingModal({
+ open,
+ onClose,
+ isMobile,
+ indices = [],
+ selectedCodes = [],
+ onChangeSelected,
+ onResetDefault,
+}) {
+ const selectedList = useMemo(() => {
+ if (!indices?.length || !selectedCodes?.length) return [];
+ const map = new Map(indices.map((it) => [it.code, it]));
+ return selectedCodes
+ .map((code) => map.get(code))
+ .filter(Boolean);
+ }, [indices, selectedCodes]);
+
+ const allIndices = indices || [];
+ const selectedSet = useMemo(
+ () => new Set(selectedCodes || []),
+ [selectedCodes]
+ );
+
+ const [resetConfirmOpen, setResetConfirmOpen] = useState(false);
+
+ const handleToggleCode = (code) => {
+ if (!code) return;
+ if (selectedSet.has(code)) {
+ // 至少保留一个指数,阻止把最后一个也移除
+ if (selectedCodes.length <= 1) return;
+ const next = selectedCodes.filter((c) => c !== code);
+ onChangeSelected?.(next);
+ } else {
+ const next = [...selectedCodes, code];
+ onChangeSelected?.(next);
+ }
+ };
+
+ const handleReorder = (newOrder) => {
+ onChangeSelected?.(newOrder);
+ };
+
+ const body = (
+
+
+
+
+
已添加指数
+
+ 拖动下方指数即可排序
+
+
+
+
+ {selectedList.length === 0 ? (
+
+ 暂未添加指数,请在下方选择想要关注的指数。
+
+ ) : (
+
+
+ {selectedList.map((item) => {
+ const isUp = item.change >= 0;
+ const color =
+ isUp ? "var(--danger)" : "var(--success)";
+ return (
+
+ {selectedCodes.length > 1 && (
+
+ )}
+
+ {item.name}
+
+
+ {item.price?.toFixed
+ ? item.price.toFixed(2)
+ : String(item.price ?? "-")}
+
+
+ {(item.change >= 0 ? "+" : "") +
+ item.change.toFixed(2)}{" "}
+ {(item.changePercent >= 0 ? "+" : "") +
+ item.changePercent.toFixed(2)}
+ %
+
+
+ );
+ })}
+
+
+ )}
+
+
+
+
+
+ 点击即可选指数
+
+ {onResetDefault && (
+
+ )}
+
+
+
+ {allIndices.map((item) => {
+ const active = selectedSet.has(item.code);
+ return (
+
+ );
+ })}
+
+
+
+ );
+
+ if (!open) return null;
+
+ if (isMobile) {
+ return (
+ {
+ if (!v) onClose?.();
+ }}
+ direction="bottom"
+ >
+
+
+
+
+ 指数个性化设置
+
+
+
+
+
+ {body}
+
+
+ {resetConfirmOpen && (
+
+ }
+ confirmVariant="primary"
+ confirmText="恢复默认"
+ onConfirm={() => {
+ onResetDefault?.();
+ setResetConfirmOpen(false);
+ }}
+ onCancel={() => setResetConfirmOpen(false)}
+ />
+ )}
+
+
+ );
+ }
+
+ return (
+
+ );
+}
+
diff --git a/app/globals.css b/app/globals.css
index 2de65d4..d65a052 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -168,6 +168,13 @@ body::before {
width: 1200px;
margin: 0 auto;
padding: 24px;
+ /* 隐藏 y 轴滚动条,保留滚动能力 */
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+}
+.container::-webkit-scrollbar {
+ width: 0;
+ display: none;
}
.page-width-slider {
@@ -978,6 +985,13 @@ input[type="number"] {
width: 100%;
max-width: 100%;
overflow-x: clip;
+ /* 移动端同样隐藏 y 轴滚动条 */
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+ }
+ .container::-webkit-scrollbar {
+ width: 0;
+ display: none;
}
.grid {
@@ -1890,7 +1904,6 @@ input[type="number"] {
@media (max-width: 640px) {
.filter-bar {
position: sticky;
- top: 60px; /* Navbar height */
z-index: 40;
width: calc(100% + 32px);
background: rgba(15, 23, 42, 0.9);
diff --git a/app/page.jsx b/app/page.jsx
index f34ef94..9504f32 100644
--- a/app/page.jsx
+++ b/app/page.jsx
@@ -59,6 +59,7 @@ import UpdatePromptModal from "./components/UpdatePromptModal";
import RefreshButton from "./components/RefreshButton";
import WeChatModal from "./components/WeChatModal";
import DcaModal from "./components/DcaModal";
+import MarketIndexAccordion from "./components/MarketIndexAccordion";
import githubImg from "./assets/github.svg";
import { supabase, isSupabaseConfigured } from './lib/supabase';
import { toast as sonnerToast } from 'sonner';
@@ -243,6 +244,7 @@ export default function HomePage() {
const containerRef = useRef(null);
const [navbarHeight, setNavbarHeight] = useState(0);
const [filterBarHeight, setFilterBarHeight] = useState(0);
+ const [marketIndexAccordionHeight, setMarketIndexAccordionHeight] = useState(0);
// 主题初始固定为 dark,避免 SSR 与客户端首屏不一致导致 hydration 报错;真实偏好由 useLayoutEffect 在首帧前恢复
const [theme, setTheme] = useState('dark');
const [showThemeTransition, setShowThemeTransition] = useState(false);
@@ -3784,10 +3786,15 @@ export default function HomePage() {
-
+
-
+
setMaskAmounts((v) => !v)}
/>
@@ -4007,7 +4014,7 @@ export default function HomePage() {
;
+}
+
+function AccordionItem({
+ className,
+ ...props
+}) {
+ return (
+
+ );
+}
+
+function AccordionTrigger({
+ className,
+ children,
+ ...props
+}) {
+ return (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}>
+ {children}
+
+
+
+ );
+}
+
+function AccordionContent({
+ className,
+ children,
+ ...props
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }