diff --git a/apps/desktop/layer/renderer/src/modules/discover/DiscoverImport.tsx b/apps/desktop/layer/renderer/src/modules/discover/DiscoverImport.tsx index e51ecf30f..12bc1b5f8 100644 --- a/apps/desktop/layer/renderer/src/modules/discover/DiscoverImport.tsx +++ b/apps/desktop/layer/renderer/src/modules/discover/DiscoverImport.tsx @@ -5,7 +5,6 @@ import { AccordionTrigger, } from "@follow/components/ui/accordion/index.js" import { Button } from "@follow/components/ui/button/index.js" -import { Card, CardContent, CardHeader } from "@follow/components/ui/card/index.jsx" import { Form, FormControl, @@ -14,27 +13,34 @@ import { FormMessage, } from "@follow/components/ui/form/index.jsx" import { Input } from "@follow/components/ui/input/index.js" -import { cn } from "@follow/utils/utils" +import type { BizRespose } from "@follow/models" import { zodResolver } from "@hookform/resolvers/zod" import { useMutation } from "@tanstack/react-query" -import { Fragment } from "react/jsx-runtime" +import { Fragment } from "react" import { useForm } from "react-hook-form" import { Trans, useTranslation } from "react-i18next" import { z } from "zod" import { DropZone } from "~/components/ui/drop-zone" import { Media } from "~/components/ui/media" +import { useModalStack } from "~/components/ui/modal/stacked/hooks" import { apiFetch } from "~/lib/api-fetch" import { toastFetchError } from "~/lib/error-parser" -import { Queries } from "~/queries" -import { FollowSummary } from "../feed/feed-summary" +import { OpmlSelectionModal } from "./OpmlSelectionModal" +import type { ParsedOpmlData } from "./types" -type FeedResponseList = { - id: string - url: string - title: string | null -}[] +const parseOpmlFile = async (file: File): Promise => { + const formData = new FormData() + formData.append("file", file) + + const data = await apiFetch>("/subscriptions/parse-opml", { + method: "POST", + body: formData, + }) + + return data.data +} const formSchema = z.object({ file: z @@ -47,64 +53,32 @@ const formSchema = z.object({ }), }) -const NumberDisplay = ({ value }) => {value ?? 0} - -const list: { - key: string - title: I18nKeys - className: string -}[] = [ - { - key: "parsedErrorItems", - title: "discover.import.parsedErrorItems", - className: "text-red-500", - }, - { - key: "successfulItems", - title: "discover.import.successfulItems", - className: "text-green-500", - }, - { - key: "conflictItems", - title: "discover.import.conflictItems", - className: "text-yellow-500", - }, -] - export function DiscoverImport() { const form = useForm>({ resolver: zodResolver(formSchema), }) - const mutation = useMutation({ - mutationFn: async (file: File) => { - const formData = new FormData() - formData.append("file", file) - // FIXME: if post data is form data, hono hc not support this. + const { present } = useModalStack() - const { data } = await apiFetch<{ - data: { - successfulItems: FeedResponseList - conflictItems: FeedResponseList - parsedErrorItems: FeedResponseList - } - }>("/subscriptions/import", { - method: "POST", - body: formData, - }) - - return data - }, - onSuccess: () => { - Queries.subscription.all().invalidateRoot() - }, + const parseOpmlMutation = useMutation({ + mutationFn: parseOpmlFile, async onError(err) { toastFetchError(err) }, }) function onSubmit(values: z.infer) { - mutation.mutate(values.file) + parseOpmlMutation.mutate(values.file, { + onSuccess: (parsedData) => { + present({ + title: t("discover.import.preview_opml_content"), + content: () => , + clickOutsideToDismiss: false, + modalClassName: "max-w-2xl w-full h-[80vh]", + modalContentClassName: "flex flex-col h-full", + }) + }, + }) } const { t } = useTranslation() @@ -180,7 +154,7 @@ export function DiscoverImport() { - + {t("discover.import.opml_step1_other")} @@ -230,47 +204,13 @@ export function DiscoverImport() { - {mutation.isSuccess && ( -
- - - , - ConflictNum: , - ErrorNum: , - }} - /> - - - {list.map((item) => ( -
-
- {t(item.title)} -
-
- {!mutation.data?.[item.key].length && ( -
{t("discover.import.noItems")}
- )} - {mutation.data?.[item.key].map((feed) => ( - - ))} -
-
- ))} -
-
-
- )} ) } diff --git a/apps/desktop/layer/renderer/src/modules/discover/OpmlSelectionModal.tsx b/apps/desktop/layer/renderer/src/modules/discover/OpmlSelectionModal.tsx new file mode 100644 index 000000000..bb3e4f2ba --- /dev/null +++ b/apps/desktop/layer/renderer/src/modules/discover/OpmlSelectionModal.tsx @@ -0,0 +1,373 @@ +import { Button } from "@follow/components/ui/button/index.js" +import { Checkbox } from "@follow/components/ui/checkbox/index.jsx" +import { Input } from "@follow/components/ui/input/index.js" +import { ScrollArea } from "@follow/components/ui/scroll-area/index.js" +import { Tooltip, TooltipContent, TooltipTrigger } from "@follow/components/ui/tooltip/index.jsx" +import type { BizRespose } from "@follow/models" +import { cn } from "@follow/utils/utils" +import { useMutation } from "@tanstack/react-query" +import Fuse from "fuse.js" +import { useCallback, useMemo, useState } from "react" +import { Trans, useTranslation } from "react-i18next" +import { toast } from "sonner" + +import { useCurrentModal } from "~/components/ui/modal/stacked/hooks" +import { apiFetch } from "~/lib/api-fetch" +import { toastFetchError } from "~/lib/error-parser" +import { Queries } from "~/queries" + +import type { ParsedFeedItem, ParsedOpmlData } from "./types" + +type FeedResponseList = { + id: string + url: string + title: string | null +}[] +export const OpmlSelectionModal = ({ + parsedData, + + file, +}: { + parsedData: ParsedOpmlData + + file: File +}) => { + const { dismiss } = useCurrentModal() + + const importMutation = useMutation({ + mutationFn: async (selectedItems: ParsedFeedItem[]) => { + const formData = new FormData() + + formData.append("file", file) + formData.append("items", JSON.stringify(selectedItems.map((i) => i.url))) + + const { data } = await apiFetch< + BizRespose<{ + successfulItems: FeedResponseList + conflictItems: FeedResponseList + parsedErrorItems: FeedResponseList + }> + >("/subscriptions/import", { + method: "POST", + body: formData, + }) + + return data + }, + onSuccess: (data) => { + Queries.subscription.all().invalidateRoot() + + const { successfulItems, conflictItems, parsedErrorItems } = data + + if (parsedErrorItems.length > 0) { + toast.warning(t("discover.import.import_completed_with_issues"), { + description: ( + , + ConflictNum: , + ErrorNum: , + br:
, + }} + /> + ), + duration: 5000, + }) + } else { + dismiss() + // Show success if everything went well + toast.success(t("discover.import.import_successful"), { + description: ( + , + ConflictNum: , + ErrorNum: , + br:
, + }} + /> + ), + duration: 5000, + }) + } + }, + async onError(err) { + toastFetchError(err) + }, + }) + + const { t } = useTranslation() + const [searchQuery, setSearchQuery] = useState("") + const [selectedItems, setSelectedItems] = useState>( + () => new Set(parsedData.subscriptions.map((_, index) => index.toString())), + ) + + const fuse = useMemo(() => { + return new Fuse(parsedData.subscriptions, { + keys: [ + { name: "title", weight: 0.7 }, + { name: "url", weight: 0.2 }, + { name: "category", weight: 0.1 }, + ], + threshold: 0.3, + includeMatches: true, + minMatchCharLength: 2, + }) + }, [parsedData.subscriptions]) + + const filteredSubscriptions = useMemo(() => { + if (!searchQuery.trim()) { + return parsedData.subscriptions.map((item, index) => ({ item, refIndex: index })) + } + + return fuse.search(searchQuery).map((result) => ({ + item: result.item, + refIndex: result.refIndex, + })) + }, [fuse, searchQuery, parsedData.subscriptions]) + + const selectedCount = selectedItems.size + const isQuotaExceeded = selectedCount > parsedData.remaining + const quotaWarningThreshold = Math.max(1, Math.floor(parsedData.remaining * 0.8)) // 80% of quota + + const toggleItem = useCallback((index: string) => { + setSelectedItems((prev) => { + const newSet = new Set(prev) + if (newSet.has(index)) { + newSet.delete(index) + } else { + newSet.add(index) + } + return newSet + }) + }, []) + + const toggleAll = useCallback( + (checked: boolean) => { + if (checked) { + // Select all filtered items, but respect quota + const filteredIndices = filteredSubscriptions.map(({ refIndex }) => refIndex.toString()) + setSelectedItems((prev) => { + const newSet = new Set(prev) + + // If we would exceed quota, only select up to the remaining limit + let addedCount = 0 + for (const index of filteredIndices) { + if (newSet.size + addedCount >= parsedData.remaining) { + break + } + if (!newSet.has(index)) { + addedCount++ + } + newSet.add(index) + } + return newSet + }) + } else { + // Deselect all filtered items - no quota restrictions for deselection + const filteredIndices = new Set( + filteredSubscriptions.map(({ refIndex }) => refIndex.toString()), + ) + setSelectedItems((prev) => { + const newSet = new Set(prev) + filteredIndices.forEach((index) => newSet.delete(index)) + return newSet + }) + } + }, + [filteredSubscriptions, parsedData.remaining], + ) + + const handleImport = useCallback(() => { + const selected = parsedData.subscriptions.filter((_, index) => + selectedItems.has(index.toString()), + ) + importMutation.mutate(selected) + }, [parsedData.subscriptions, selectedItems, importMutation]) + + // Calculate selection states for filtered items + const filteredSelectedCount = filteredSubscriptions.filter(({ refIndex }) => + selectedItems.has(refIndex.toString()), + ).length + + const allFilteredSelected = + filteredSelectedCount === filteredSubscriptions.length && filteredSubscriptions.length > 0 + const someFilteredSelected = + filteredSelectedCount > 0 && filteredSelectedCount < filteredSubscriptions.length + + return ( +
+
+

+ {t("discover.import.select_feeds_to_import")} +

+

+ {t("discover.import.select_feeds_description")} +

+
+ + {/* Quota Status */} +
= quotaWarningThreshold + ? "border-yellow-200 bg-yellow-50 dark:border-yellow-800 dark:bg-yellow-950" + : "border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950", + )} + > +
+ + +
= quotaWarningThreshold + ? "text-yellow" + : "text-green", + )} + > + = quotaWarningThreshold + ? "i-mgc-warning-cute-re" + : "i-mgc-check-circle-cute-re", + )} + /> +
+
+ + {isQuotaExceeded ? ( +

{t("discover.import.quota_exceeded_warning")}

+ ) : selectedCount >= quotaWarningThreshold ? ( +

+ {t("discover.import.quota_warning", { + remaining: parsedData.remaining - selectedCount, + })} +

+ ) : ( +

+ {t("discover.import.remaining_quota", { + remaining: parsedData.remaining - selectedCount, + })} +

+ )} +
+
+ + {t("discover.import.quota_status")} {selectedCount}/{parsedData.remaining} + +
+
+ + {/* Search Input */} +
+ setSearchQuery(e.target.value)} + className="w-full" + /> +
+ + + + +
+ {filteredSubscriptions.length === 0 && searchQuery.trim() ? ( +
+ {t("discover.import.no_feeds_found", "No feeds found matching your search.")} +
+ ) : ( + filteredSubscriptions.map(({ item, refIndex }) => { + const isSelected = selectedItems.has(refIndex.toString()) + const wouldExceedQuota = !isSelected && selectedCount >= parsedData.remaining + + return ( +
!wouldExceedQuota && toggleItem(refIndex.toString())} + > + !wouldExceedQuota && toggleItem(refIndex.toString())} + disabled={wouldExceedQuota} + /> +
+
{item.title || "Untitled Feed"}
+
{item.url}
+ {item.category && ( +
+ {item.category} +
+ )} +
+
+ ) + }) + )} +
+
+ +
+ + +
+
+ ) +} + +const NumberDisplay = ({ value }) => {value ?? 0} diff --git a/apps/desktop/layer/renderer/src/modules/discover/types.ts b/apps/desktop/layer/renderer/src/modules/discover/types.ts index 87ba90b63..43bd63659 100644 --- a/apps/desktop/layer/renderer/src/modules/discover/types.ts +++ b/apps/desktop/layer/renderer/src/modules/discover/types.ts @@ -1 +1,19 @@ +import type { FeedViewType } from "@follow/constants" + export * from "@follow/models/rsshub" + +export type ParsedFeedItem = { + url: string + title: string | null + category?: string | null +} + +export type ParsedOpmlData = { + remaining: number + subscriptions: { + category: string | null + title: string + url: string + view: FeedViewType + }[] +} diff --git a/apps/desktop/layer/renderer/src/modules/settings/sections/fonts.tsx b/apps/desktop/layer/renderer/src/modules/settings/sections/fonts.tsx index 6a2029d8a..31104cce1 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/sections/fonts.tsx +++ b/apps/desktop/layer/renderer/src/modules/settings/sections/fonts.tsx @@ -41,7 +41,23 @@ const useFontDataWeb = () => { return [ { label: t("appearance.content_font.default"), value: "inherit" }, { label: t("appearance.font.system"), value: "system-ui" }, - ...["Arial", "PingFang SC", "Microsoft YaHei", "SF Pro"].map((font) => ({ + ...[ + // English + "SF Pro", + "Segoe UI", + "Helvetica", + "Arial", + // Chinese + "PingFang SC", + "PingFang TC", + "PingFang HK", + + "Microsoft YaHei", + "Microsoft JhengHei", + // Japanese + "Yu Gothic", + "Hiragino Sans", + ].map((font) => ({ label: font, value: font, diff --git a/locales/app/en.json b/locales/app/en.json index ff90e93d1..2ff7c1c38 100644 --- a/locales/app/en.json +++ b/locales/app/en.json @@ -44,7 +44,10 @@ "discover.feed_maintainers": "This feed is provided by RSSHub, with credit to ", "discover.import.click_to_upload": "Click to upload OPML file", "discover.import.conflictItems": "Conflict Items", + "discover.import.import_completed_with_issues": "Import completed with some issues", + "discover.import.import_successful": "Import completed successfully", "discover.import.noItems": "No items", + "discover.import.no_feeds_found": "No feeds found matching your search.", "discover.import.opml": "OPML file", "discover.import.opml_step1": "Export OPML from your RSS reader", "discover.import.opml_step1_feedly": "Export OPML from Feedly", @@ -57,8 +60,21 @@ "discover.import.opml_step1_other": "Export OPML from other readers", "discover.import.opml_step1_other_step1": "OPML is a widely supported open format and is essentially the standard for sharing feed subscription lists. Nearly all RSS readers allow for OPML import and export. If you're unsure how to do this, please refer to your RSS reader's help manual or join our community for assistance.", "discover.import.opml_step2": "Import OPML to Folo", + "discover.import.parse_opml": "Parse OPML", "discover.import.parsedErrorItems": "Parsed Error Items", - "discover.import.result": " feeds were successfully imported, were already subscribed to, and failed to import.", + "discover.import.preview_opml_content": "Preview OPML Content", + "discover.import.quota_exceeded": "Quota exceeded", + "discover.import.quota_exceeded_warning": "You have selected more feeds than your remaining quota allows. Please deselect some feeds to continue.", + "discover.import.quota_limit_reached": "Quota limit reached", + "discover.import.quota_status": "Import quota:", + "discover.import.quota_warning": "You have {{remaining}} feeds remaining in your quota.", + "discover.import.remaining_quota": "You can import {{remaining}} more feeds", + "discover.import.result": " feeds were successfully imported.
were already subscribed to.
failed to import.", + "discover.import.search_feeds_placeholder": "Search feeds...", + "discover.import.select_all_feeds": "Select all feeds", + "discover.import.select_all_filtered": "Select all filtered", + "discover.import.select_feeds_description": "Review and select which feeds you want to import. All feeds are selected by default.", + "discover.import.select_feeds_to_import": "Select feeds to import", "discover.import.successfulItems": "Successful Items", "discover.inbox.actions": "Actions", "discover.inbox.description": "You can receive information via email and webhooks through the inbox.", diff --git a/locales/app/ja.json b/locales/app/ja.json index b73c624eb..fa8c1ed9f 100644 --- a/locales/app/ja.json +++ b/locales/app/ja.json @@ -44,9 +44,21 @@ "discover.import.click_to_upload": "OPML ファイルをアップロードするにはクリック", "discover.import.conflictItems": "重複アイテム", "discover.import.noItems": "アイテムなし", + "discover.import.no_feeds_found": "検索条件に一致するフィードが見つかりませんでした。", "discover.import.opml": "OPML ファイル", "discover.import.parsedErrorItems": "パースエラーのアイテム", + "discover.import.quota_exceeded": "枠を超過", + "discover.import.quota_exceeded_warning": "選択したフィード数が残り枠を超えています。続行するには一部のフィードの選択を解除してください。", + "discover.import.quota_limit_reached": "枠制限に達しました", + "discover.import.quota_status": "インポート枠:", + "discover.import.quota_warning": "残り {{remaining}} フィードまでインポートできます。", + "discover.import.remaining_quota": "あと {{remaining}} フィードをインポートできます", "discover.import.result": " フィードのインポートに成功しました、 件を購読中、インポート失敗は 件。", + "discover.import.search_feeds_placeholder": "フィードを検索...", + "discover.import.select_all_feeds": "すべてのフィードを選択", + "discover.import.select_all_filtered": "フィルタ結果をすべて選択", + "discover.import.select_feeds_description": "インポートするフィードを確認して選択してください。デフォルトですべて選択されています。", + "discover.import.select_feeds_to_import": "インポートするフィードを選択", "discover.import.successfulItems": "成功したアイテム", "discover.inbox.actions": "アクション", "discover.inbox.description": "情報は email か webhooks を通して受信箱に配信されます。", diff --git a/locales/app/zh-CN.json b/locales/app/zh-CN.json index c17be09bc..911dd0a72 100644 --- a/locales/app/zh-CN.json +++ b/locales/app/zh-CN.json @@ -44,7 +44,10 @@ "discover.feed_maintainers": "由 RSSHub 提供,感谢贡献者 ", "discover.import.click_to_upload": "导入 OPML 文件", "discover.import.conflictItems": "冲突项目", + "discover.import.import_completed_with_issues": "导入完成,但存在一些问题", + "discover.import.import_successful": "导入成功完成", "discover.import.noItems": "没有项目", + "discover.import.no_feeds_found": "未找到匹配的订阅源。", "discover.import.opml": "OPML 文件", "discover.import.opml_step1": "从你的 RSS 阅读器导出 OPML 文件", "discover.import.opml_step1_feedly": "从 Feedly 导出 OPML 文件", @@ -57,8 +60,20 @@ "discover.import.opml_step1_other": "从其他阅读器导出 OPML 文件", "discover.import.opml_step1_other_step1": "OPML 是一种广泛支持的开放格式,基本上是共享订阅源列表的标准。几乎所有的 RSS 阅读器都允许导入和导出 OPML。如果您不确定如何操作,请参考您的 RSS 阅读器的帮助手册或加入我们的社区以获取帮助。", "discover.import.opml_step2": "导入 OPML 到 Folo", + "discover.import.parse_opml": "解析 OPML", "discover.import.parsedErrorItems": "解析错误项目", - "discover.import.result": "成功导入了 个订阅源, 个已订阅, 个导入失败。", + "discover.import.preview_opml_content": "预览 OPML 内容", + "discover.import.quota_exceeded": "超出额度", + "discover.import.quota_exceeded_warning": "你选择的订阅源数量超过了剩余额度。请取消选择一些订阅源以继续。", + "discover.import.quota_limit_reached": "已达额度限制", + "discover.import.quota_status": "导入额度:", + "discover.import.quota_warning": "你的额度还剩 {{remaining}} 个订阅源。", + "discover.import.remaining_quota": "你还可以导入 {{remaining}} 个订阅源", + "discover.import.search_feeds_placeholder": "搜索订阅源...", + "discover.import.select_all_feeds": "全选订阅源", + "discover.import.select_all_filtered": "全选筛选结果", + "discover.import.select_feeds_description": "检查并选择你想要导入的订阅源。默认全部选中。", + "discover.import.select_feeds_to_import": "选择要导入的订阅源", "discover.import.successfulItems": "成功项目", "discover.inbox.actions": "操作", "discover.inbox.description": "你可以通过邮件和 Webhook 在收件箱接收信息。", @@ -263,14 +278,14 @@ "new_user_guide.step.behavior.unread_question.option1": "主动:显示时自动标记为已读。", "new_user_guide.step.behavior.unread_question.option2": "平衡:悬停或滚出视野时自动标记为已读。", "new_user_guide.step.behavior.unread_question.option3": "被动:仅在点击时标记为已读。", - "new_user_guide.step.discover.description": "你也可以稍后在“发现”中找到它们。", + "new_user_guide.step.discover.description": "你也可以稍后在\"发现\"中找到它们。", "new_user_guide.step.discover.title": "为你推荐", "new_user_guide.step.features.actions.description": "自动化规则允许你对不同的订阅执行不同的操作。\n- 使用 AI 进行总结或翻译\n- 配置阅读条目的方式\n- 启用新条目的通知或静音\n- 重写或屏蔽特定条目\n- 将新条目发送到 webhook 地址", "new_user_guide.step.features.integration.description": "集成允许你将条目保存到其他服务。目前支持的服务有:\n- Eagle\n- Readwise\n- Instapaper\n- Obsidian\n- Outline\n- Readeck", - "new_user_guide.step.migrate.description": "您也可以稍后在“发现”中导入它们。", + "new_user_guide.step.migrate.description": "您也可以稍后在\"发现\"中导入它们。", "new_user_guide.step.migrate.title": "从其他 RSS 阅读器迁移", "new_user_guide.step.power.description": "Folo 使用区块链技术作为活跃用户和优秀创作者的激励机制。用户可以通过持有和使用 Power 来获得更多服务和福利。创作者可以通过提供高质量的内容和服务来获得更多奖励。", - "new_user_guide.step.profile.description": "你也可以稍后在“设置”中设置它。", + "new_user_guide.step.profile.description": "你也可以稍后在\"设置\"中设置它。", "new_user_guide.step.profile.title": "设置你的个人资料", "new_user_guide.step.shortcuts.description1": "快捷键让你更方便、高效地使用 Folo", "new_user_guide.step.shortcuts.description2": "随时按 快速查看所有快捷键。", diff --git a/locales/app/zh-TW.json b/locales/app/zh-TW.json index 227095e3e..edde3ab1b 100644 --- a/locales/app/zh-TW.json +++ b/locales/app/zh-TW.json @@ -43,10 +43,23 @@ "discover.feed_maintainers": "由 RSSHub 提供,感謝 的支持", "discover.import.click_to_upload": "點擊上傳 OPML 文件", "discover.import.conflictItems": "衝突項目", + "discover.import.import_completed_with_issues": "匯入完成,但存在一些問題", + "discover.import.import_successful": "匯入成功完成", "discover.import.noItems": "沒有項目", + "discover.import.no_feeds_found": "未找到符合搜尋條件的 RSS 摘要。", "discover.import.opml": "OPML 檔案", "discover.import.parsedErrorItems": "解析錯誤項目", - "discover.import.result": "成功匯入了 個 RSS 摘要, 個已經訂閱, 個匯入失敗。", + "discover.import.quota_exceeded": "超出額度", + "discover.import.quota_exceeded_warning": "您選擇的 RSS 摘要數量超過了剩餘額度。請取消選擇一些 RSS 摘要以繼續。", + "discover.import.quota_limit_reached": "已達額度限制", + "discover.import.quota_status": "匯入額度:", + "discover.import.quota_warning": "您的額度還剩 {{remaining}} 個 RSS 摘要。", + "discover.import.remaining_quota": "你還可以匯入 {{remaining}} 個 RSS 摘要", + "discover.import.search_feeds_placeholder": "搜尋 RSS 摘要...", + "discover.import.select_all_feeds": "全選 RSS 摘要", + "discover.import.select_all_filtered": "全選篩選結果", + "discover.import.select_feeds_description": "檢視並選擇您想要匯入的 RSS 摘要。預設全部選中。", + "discover.import.select_feeds_to_import": "選擇要匯入的 RSS 摘要", "discover.import.successfulItems": "成功項目", "discover.inbox.actions": "操作", "discover.inbox.description": "你可以透過電子信箱和 Webhooks 在收件匣接收資訊。", diff --git a/packages/internal/components/src/ui/checkbox/index.tsx b/packages/internal/components/src/ui/checkbox/index.tsx index cdec7eea1..302ce8e6d 100644 --- a/packages/internal/components/src/ui/checkbox/index.tsx +++ b/packages/internal/components/src/ui/checkbox/index.tsx @@ -6,9 +6,12 @@ import type { HTMLMotionProps } from "motion/react" import { m } from "motion/react" import * as React from "react" -type CheckboxProps = React.ComponentProps & HTMLMotionProps<"button"> +type CheckboxProps = React.ComponentProps & + HTMLMotionProps<"button"> & { + indeterminate?: boolean + } -function Checkbox({ className, onCheckedChange, ...props }: CheckboxProps) { +function Checkbox({ className, onCheckedChange, indeterminate, ...props }: CheckboxProps) { const [isChecked, setIsChecked] = React.useState(props?.checked ?? props?.defaultChecked ?? false) React.useEffect(() => { @@ -29,6 +32,7 @@ function Checkbox({ className, onCheckedChange, ...props }: CheckboxProps) { data-slot="checkbox" className={cn( "bg-fill cursor-checkbox focus-visible:ring-border data-[state=checked]:bg-accent peer flex size-5 shrink-0 items-center justify-center rounded-sm transition-colors duration-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:text-white", + indeterminate && "bg-accent text-white", className, )} whileTap={{ scale: 0.95 }} @@ -36,39 +40,75 @@ function Checkbox({ className, onCheckedChange, ...props }: CheckboxProps) { {...props} > - - + - + }} + /> + + ) : ( + + + + )} diff --git a/packages/internal/models/src/types.ts b/packages/internal/models/src/types.ts index 484cfe213..96dad0c98 100644 --- a/packages/internal/models/src/types.ts +++ b/packages/internal/models/src/types.ts @@ -177,3 +177,8 @@ export type EntryReadHistoriesModel = Optional< > & { entryId: string } + +export type BizRespose = { + data: T + code: 0 +} diff --git a/packages/internal/shared/src/hono.ts b/packages/internal/shared/src/hono.ts index cb254f209..052a84f82 100644 --- a/packages/internal/shared/src/hono.ts +++ b/packages/internal/shared/src/hono.ts @@ -19246,6 +19246,27 @@ declare const _routes: hono_hono_base.HonoBase | hono_types.MergeSchemaPath