diff --git a/package.json b/package.json index 9a9c47c0..958efdd7 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-select": "^2.1.6", "@radix-ui/react-switch": "^1.1.3", + "@radix-ui/react-toast": "^1.2.13", "@radix-ui/react-toggle": "^1.1.6", "@radix-ui/react-toggle-group": "^1.1.7", "@testing-library/user-event": "^14.6.0", diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index 0854b3d7..b871e4d3 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -721,5 +721,17 @@ }, "help_pageAction_share_desc": { "message": "You can share and get Page Action commands from the Selection Command Hub." + }, + "review_request_title": { + "message": "Enjoying Selection Command?" + }, + "review_request_message": { + "message": "If you find this extension helpful, please consider leaving a review on the Chrome Web Store. Your feedback helps us improve!" + }, + "review_request_button": { + "message": "Write a Review" + }, + "review_request_close": { + "message": "Not Now" } -} +} \ No newline at end of file diff --git a/public/_locales/ja/messages.json b/public/_locales/ja/messages.json index a69747db..3dc35d93 100644 --- a/public/_locales/ja/messages.json +++ b/public/_locales/ja/messages.json @@ -718,5 +718,17 @@ }, "help_pageAction_share_desc": { "message": "Selection Command Hubから、Page Actionコマンドの共有と取得ができます。 " + }, + "review_request_title": { + "message": "Selection Command はいかがですか?" + }, + "review_request_message": { + "message": "この拡張機能を気に入っていただけたら、Chrome Web Storeでのレビューをお願いします。皆様のフィードバックが改善の糧となります!" + }, + "review_request_button": { + "message": "レビューする" + }, + "review_request_later": { + "message": "後で" } -} +} \ No newline at end of file diff --git a/src/background_script.ts b/src/background_script.ts index de8051ae..94985751 100644 --- a/src/background_script.ts +++ b/src/background_script.ts @@ -18,10 +18,45 @@ import type { } from '@/types' import { Storage, SESSION_STORAGE_KEY } from './services/storage' -const OPTION_PAGE = 'src/options_page.html' +const CONSTANTS = { + OPTION_PAGE: 'src/options_page.html', + REVIEW_THRESHOLD: 100, + REVIEW_INTERVAL: 50, + SETTING_KEY: { + COMMAND_EXECUTION_COUNT: 'commandExecutionCount', + HAS_SHOWN_REVIEW_REQUEST: 'hasShownReviewRequest', + }, +} as const BgData.init() +// Increment command execution count and check review request +const incrementCommandExecutionCount = async (): Promise => { + try { + const settings = await Settings.get() + let count = settings.commandExecutionCount ?? 0 + const hasShown = settings.hasShownReviewRequest ?? false + + // Increment command execution count + count++ + await Settings.update( + CONSTANTS.SETTING_KEY.COMMAND_EXECUTION_COUNT, + () => count, + true, + ) + + // Show review request when threshold is exceeded + if ((count === CONSTANTS.REVIEW_THRESHOLD || (count > CONSTANTS.REVIEW_THRESHOLD && (count - CONSTANTS.REVIEW_THRESHOLD) % CONSTANTS.REVIEW_INTERVAL === 0)) && !hasShown) { + const tabs = await chrome.tabs.query({ active: true }) + if (tabs[0]?.id) { + await Ipc.sendTab(tabs[0].id, TabCommand.showReviewRequest) + } + } + } catch (error) { + console.error('Failed to increment command execution count:', error) + } +} + type Sender = chrome.runtime.MessageSender export type openPopupAndClickProps = OpenPopupsProps & { @@ -68,29 +103,50 @@ function bindVariables( } const commandFuncs = { - [BgCommand.openPopups]: (param: OpenPopupsProps): boolean => { - openPopups(param) - return false + [BgCommand.openPopups]: ( + param: OpenPopupsProps, + _: Sender, + response: (res: unknown) => void, + ): boolean => { + incrementCommandExecutionCount().then(async () => { + try { + await openPopups(param) + response(true) + } catch (error) { + console.error('Failed to execute openPopups:', error) + response(false) + } + }) + return true }, - [BgCommand.openPopupAndClick]: (param: openPopupAndClickProps): boolean => { - const open = async () => { - const tabIds = await openPopups(param) - if (tabIds.length > 0) { - await Ipc.sendQueue(tabIds[0], TabCommand.clickElement, { - selector: (param as { selector: string }).selector, - }) - return + [BgCommand.openPopupAndClick]: ( + param: openPopupAndClickProps, + _: Sender, + response: (res: unknown) => void, + ): boolean => { + incrementCommandExecutionCount().then(async () => { + try { + const tabIds = await openPopups(param) + if (tabIds.length > 0) { + await Ipc.sendQueue(tabIds[0], TabCommand.clickElement, { + selector: (param as { selector: string }).selector, + }) + } else { + console.debug('tab not found') + } + response(true) + } catch (error) { + console.error('Failed to execute openPopupAndClick:', error) + response(false) } - console.debug('tab not found') - } - open() - return false + }) + return true }, [BgCommand.openOption]: (): boolean => { chrome.tabs.create({ - url: OPTION_PAGE, + url: CONSTANTS.OPTION_PAGE, }) return false }, @@ -110,9 +166,9 @@ const commandFuncs = { await Settings.set({ ...settings, pageRules, - }) + }, true) chrome.tabs.create({ - url: `${OPTION_PAGE}#pageRules`, + url: `${CONSTANTS.OPTION_PAGE}#pageRules`, }) } add() @@ -130,24 +186,24 @@ const commandFuncs = { const cmd = isSearch ? { + id: params.id, + title: params.title, + searchUrl: params.searchUrl, + iconUrl: params.iconUrl, + openMode: params.openMode, + openModeSecondary: params.openModeSecondary, + spaceEncoding: params.spaceEncoding, + popupOption: PopupOption, + } + : isPageAction + ? { id: params.id, title: params.title, - searchUrl: params.searchUrl, iconUrl: params.iconUrl, openMode: params.openMode, - openModeSecondary: params.openModeSecondary, - spaceEncoding: params.spaceEncoding, + pageActionOption: params.pageActionOption, popupOption: PopupOption, } - : isPageAction - ? { - id: params.id, - title: params.title, - iconUrl: params.iconUrl, - openMode: params.openMode, - pageActionOption: params.pageActionOption, - popupOption: PopupOption, - } : null if (!cmd) { @@ -389,7 +445,7 @@ const updateWindowSize = async ( chrome.action.onClicked.addListener(() => { chrome.tabs.create({ - url: OPTION_PAGE, + url: CONSTANTS.OPTION_PAGE, }) }) diff --git a/src/components/App.tsx b/src/components/App.tsx index fc068fb3..07862d36 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -11,12 +11,17 @@ import { getSelectionText } from '@/services/dom' import { SelectContextProvider } from '@/hooks/useSelectContext' import { PageActionContextProvider } from '@/hooks/pageAction/usePageActionContext' import { Ipc, TabCommand } from '@/services/ipc' +import { Toaster } from "@/components/ui/toaster" +import { useToast } from "@/hooks/useToast" +import { showReviewRequestToast } from '@/components/ReviewRequestToast' +import { Settings } from '@/services/settings' export function App() { const [positionElm, setPositionElm] = useState(null) const [target, setTarget] = useState(null) const [isHover, setIsHover] = useState(false) const [selectionText, setSelectionText] = useState('') + const { toast } = useToast() useEffect(() => { Ipc.addListener(TabCommand.connect, () => false) @@ -37,6 +42,21 @@ export function App() { } }, [isHover]) + useEffect(() => { + const handleShowReviewRequest = (_param: any, _sender: any, response: any) => { + showReviewRequestToast(toast, () => { + Settings.update('hasShownReviewRequest', () => true) + }) + response(true) + return true + } + + Ipc.addListener(TabCommand.showReviewRequest, handleShowReviewRequest) + return () => { + Ipc.removeListener(TabCommand.showReviewRequest) + } + }, []) + return ( @@ -50,6 +70,7 @@ export function App() { + ) diff --git a/src/components/ReviewRequestToast.tsx b/src/components/ReviewRequestToast.tsx new file mode 100644 index 00000000..b2cd6de3 --- /dev/null +++ b/src/components/ReviewRequestToast.tsx @@ -0,0 +1,39 @@ +import { t } from '@/services/i18n' +import { ToastAction } from "@/components/ui/toast" + +const REVIEW_URL = 'https://chromewebstore.google.com/detail/nlnhbibaommoelemmdfnkjkgoppkohje/reviews' +const ICON_URL = chrome.runtime.getURL('icon128.png') + +export function showReviewRequestToast(toast: any, onAccept: () => void): void { + const tst = toast({ + title: ( +

+ + + {t("review_request_title")} + +

+ ), + description: + {t("review_request_message")} + , + className: 'flex flex-col text-gray-800', + action:
+ { + // Close the toast + tst.dismiss() + }} + >{t("review_request_later")} + { + window.open(REVIEW_URL, '_blank') + onAccept() + }} + >🎉 {t("review_request_button")} +
, + duration: 60 * 1000, + }) +} \ No newline at end of file diff --git a/src/components/option/ImportExport.tsx b/src/components/option/ImportExport.tsx index e37c15ce..086d3ca6 100644 --- a/src/components/option/ImportExport.tsx +++ b/src/components/option/ImportExport.tsx @@ -1,6 +1,6 @@ import { useState, useRef } from 'react' import { Dialog } from './Dialog' -import type { SettingsType } from '@/types' +import type { UserSettings } from '@/types' import { Storage, STORAGE_KEY } from '@/services/storage' import { Settings, migrate } from '@/services/settings' @@ -24,7 +24,7 @@ function getTimestamp() { export function ImportExport() { const [resetDialog, setResetDialog] = useState(false) const [importDialog, setImportDialog] = useState(false) - const [importJson, setImportJson] = useState() + const [importJson, setImportJson] = useState() const inputFile = useRef(null) const handleReset = () => { @@ -39,7 +39,7 @@ export function ImportExport() { } const handleExport = async () => { - const data = await Storage.get(STORAGE_KEY.USER) + const data = await Storage.get(STORAGE_KEY.USER) data.commands = await Storage.getCommands() // for back compatibility @@ -84,7 +84,13 @@ export function ImportExport() { const handleImportClose = (ret: boolean) => { if (ret && importJson != null) { ; (async () => { - const data = await migrate(importJson) + const { commandExecutionCount = 0, hasShownReviewRequest = false } = await Settings.get() + const data = await migrate({ + ...importJson, + commandExecutionCount, + hasShownReviewRequest, + stars: [] + }) await Settings.set(data) location.reload() })() diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx new file mode 100644 index 00000000..5ea0563b --- /dev/null +++ b/src/components/ui/toast.tsx @@ -0,0 +1,129 @@ +"use client" + +import * as React from "react" +import * as ToastPrimitives from "@radix-ui/react-toast" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const ToastProvider = ToastPrimitives.Provider + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastViewport.displayName = ToastPrimitives.Viewport.displayName + +const toastVariants = cva( + "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", + { + variants: { + variant: { + default: "border bg-background text-foreground", + destructive: + "destructive group border-destructive bg-destructive text-destructive-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ) +}) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef + +type ToastActionElement = React.ReactElement + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +} \ No newline at end of file diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx new file mode 100644 index 00000000..b7ffff36 --- /dev/null +++ b/src/components/ui/toaster.tsx @@ -0,0 +1,35 @@ +"use client" + +import { useToast } from "@/hooks/useToast" +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/toast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ) + })} + +
+ ) +} diff --git a/src/hooks/useSetting.ts b/src/hooks/useSetting.ts index a3874545..7ecf35a8 100644 --- a/src/hooks/useSetting.ts +++ b/src/hooks/useSetting.ts @@ -29,6 +29,8 @@ const emptySettings: SettingsType = { userStyles: [], startupMethod: { method: STARTUP_METHOD.TEXT_SELECTION }, stars: [], + commandExecutionCount: 0, + hasShownReviewRequest: false, } export function useSetting(): useSettingReturn { diff --git a/src/hooks/useToast.tsx b/src/hooks/useToast.tsx new file mode 100644 index 00000000..4e4408a7 --- /dev/null +++ b/src/hooks/useToast.tsx @@ -0,0 +1,194 @@ +"use client" + +// Inspired by react-hot-toast library +import * as React from "react" + +import type { + ToastActionElement, + ToastProps, +} from "@/components/ui/toast" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1000000 + +type ToasterToast = ToastProps & { + id: string + title?: React.ReactNode + description?: React.ReactNode + action?: ToastActionElement +} + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType["ADD_TOAST"] + toast: ToasterToast + } + | { + type: ActionType["UPDATE_TOAST"] + toast: Partial + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + } + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), + } + + case "DISMISS_TOAST": { + const { toastId } = action + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t + ), + } + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + } + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + } + } +} + +const listeners: Array<(state: State) => void> = [] + +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +type Toast = Omit + +function toast({ ...props }: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + } + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + } +} + +export { useToast, toast } diff --git a/src/services/defaultSettings.ts b/src/services/defaultSettings.ts index 92533b1e..a84429e1 100644 --- a/src/services/defaultSettings.ts +++ b/src/services/defaultSettings.ts @@ -1,4 +1,4 @@ -import { SettingsType, Command } from '@/types' +import { UserSettings, Command } from '@/types' import { VERSION, OPEN_MODE, @@ -87,7 +87,7 @@ export default { }, ], stars: [], -} as SettingsType +} as UserSettings export const PopupOption = { width: 600, diff --git a/src/services/ipc.ts b/src/services/ipc.ts index ee42ef51..ea724e6e 100644 --- a/src/services/ipc.ts +++ b/src/services/ipc.ts @@ -35,6 +35,7 @@ export enum TabCommand { clickElement = 'clickElement', closeMenu = 'closeMenu', getTabId = 'getTabId', + showReviewRequest = 'showReviewRequest', // PageAction sendWindowSize = 'sendWindowSize', execPageAction = 'execPageAction', diff --git a/src/services/settings.ts b/src/services/settings.ts index c86d2b64..870e018d 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -11,7 +11,7 @@ import { SIDE, ALIGN, } from '@/const' -import type { SettingsType, Version, Command, Star } from '@/types' +import type { SettingsType, UserSettings, Version, Command, Star, UserStats } from '@/types' import { isBase64, isEmpty, @@ -57,6 +57,10 @@ export const Settings = { // Stars data.stars = await Storage.get(LOCAL_STORAGE_KEY.STARS) + // UserStats + const userStats = await Storage.get(STORAGE_KEY.USER_STATS) + data = { ...data, ...userStats } + data = await migrate(data) data.folders = data.folders.filter((folder) => !!folder.title) @@ -121,14 +125,35 @@ export const Settings = { data.commands = [] // Stars - await Storage.set(LOCAL_STORAGE_KEY.STARS, data.stars) - data.stars = [] + await Storage.set(LOCAL_STORAGE_KEY.STARS, data.stars) + + // UserStats + const userStats: UserStats = { + commandExecutionCount: data.commandExecutionCount, + hasShownReviewRequest: data.hasShownReviewRequest, + } + await Storage.set(STORAGE_KEY.USER_STATS, userStats) - await Storage.set(STORAGE_KEY.USER, data) + // Remove UserStats and stars from data + const { commandExecutionCount, hasShownReviewRequest, stars, ...restData } = data + await Storage.set(STORAGE_KEY.USER, restData) await Storage.set(LOCAL_STORAGE_KEY.CACHES, caches) return true }, + update: async ( + key: T, + updater: (value: SettingsType[T]) => SettingsType[T], + serviceWorker = false, + ): Promise => { + const settings = await Settings.get() + const updatedSettings = { + ...settings, + [key]: updater(settings[key]), + } + return Settings.set(updatedSettings, serviceWorker) + }, + addCommands: async (commands: Command[]): Promise => { const current = await Storage.getCommands() const newCommands = [...current, ...commands] diff --git a/src/services/storage.ts b/src/services/storage.ts index 3b490061..ff501ed6 100644 --- a/src/services/storage.ts +++ b/src/services/storage.ts @@ -4,6 +4,7 @@ import { Command, CaptureDataStorage } from '@/types' export enum STORAGE_KEY { USER = 0, COMMAND_COUNT = 2, + USER_STATS = 3, } export enum LOCAL_STORAGE_KEY { @@ -33,6 +34,10 @@ const DEFAULT_COUNT = -1 const DEFAULTS = { [STORAGE_KEY.USER]: DefaultSettings, [STORAGE_KEY.COMMAND_COUNT]: DEFAULT_COUNT, + [STORAGE_KEY.USER_STATS]: { + commandExecutionCount: 0, + hasShownReviewRequest: false, + }, [LOCAL_STORAGE_KEY.CACHES]: { images: {}, }, diff --git a/src/types.ts b/src/types.ts index 9936e951..8df76df9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -118,10 +118,6 @@ export type StartupMethod = { leftClickHoldParam?: number } -export type Star = { - id: string -} - export type PopupPlacement = { side: SIDE align: ALIGN @@ -129,7 +125,20 @@ export type PopupPlacement = { alignOffset: number } -export type SettingsType = { +export type Star = { + id: string +} + +type UserStars = { + stars: Array +} + +export type UserStats = { + commandExecutionCount: number + hasShownReviewRequest: boolean +} + +export type UserSettings = { settingVersion: Version startupMethod: StartupMethod popupPlacement: PopupPlacement @@ -139,9 +148,10 @@ export type SettingsType = { pageRules: Array style: STYLE userStyles: Array - stars: Array } +export type SettingsType = UserSettings & UserStats & UserStars + export type SessionData = { session_id: string timestamp: number diff --git a/yarn.lock b/yarn.lock index 8cbb4d25..e62e6fa1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -780,6 +780,16 @@ "@radix-ui/react-primitive" "2.1.0" "@radix-ui/react-slot" "1.2.0" +"@radix-ui/react-collection@1.1.6": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.6.tgz#fecf74475e4660ee99c7eb1ebfa5ccfb1a219fe4" + integrity sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-primitive" "2.1.2" + "@radix-ui/react-slot" "1.2.2" + "@radix-ui/react-compose-refs@1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz#6f766faa975f8738269ebb8a23bad4f5a8d2faec" @@ -852,6 +862,17 @@ "@radix-ui/react-use-callback-ref" "1.1.0" "@radix-ui/react-use-escape-keydown" "1.1.0" +"@radix-ui/react-dismissable-layer@1.1.9": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.9.tgz#46e025ba6e6f403677e22fbb7d99b63cf7b32bca" + integrity sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ== + dependencies: + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-primitive" "2.1.2" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-escape-keydown" "1.1.1" + "@radix-ui/react-focus-guards@1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz#8635edd346304f8b42cae86b05912b61aef27afe" @@ -1005,6 +1026,14 @@ "@radix-ui/react-primitive" "2.0.2" "@radix-ui/react-use-layout-effect" "1.1.0" +"@radix-ui/react-portal@1.1.8": + version "1.1.8" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.8.tgz#0181e85bc0d8c67229dd8cf198204f5f4cc7c09c" + integrity sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg== + dependencies: + "@radix-ui/react-primitive" "2.1.2" + "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-presence@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.2.tgz#bb764ed8a9118b7ec4512da5ece306ded8703cdc" @@ -1013,6 +1042,14 @@ "@radix-ui/react-compose-refs" "1.1.1" "@radix-ui/react-use-layout-effect" "1.1.0" +"@radix-ui/react-presence@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.4.tgz#253ac0ad4946c5b4a9c66878335f5cf07c967ced" + integrity sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-primitive@2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz#6d9efc550f7520135366f333d1e820cf225fad9e" @@ -1034,6 +1071,13 @@ dependencies: "@radix-ui/react-slot" "1.2.0" +"@radix-ui/react-primitive@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz#03f64f957719c761d22c2f92cc43ffb64bd42cc8" + integrity sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw== + dependencies: + "@radix-ui/react-slot" "1.2.2" + "@radix-ui/react-progress@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-progress/-/react-progress-1.1.2.tgz#3584c346d47f2a6f86076ce5af56ab00c66ded2b" @@ -1120,6 +1164,13 @@ dependencies: "@radix-ui/react-compose-refs" "1.1.2" +"@radix-ui/react-slot@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.2.tgz#18e6533e778a2051edc2ad0773da8e22f03f626a" + integrity sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-switch@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.3.tgz#cb6386909d1d3f65a2b81a3b15da8c91d18f49b0" @@ -1133,6 +1184,24 @@ "@radix-ui/react-use-previous" "1.1.0" "@radix-ui/react-use-size" "1.1.0" +"@radix-ui/react-toast@^1.2.13": + version "1.2.13" + resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.2.13.tgz#e2b27456b52d1b1629becb0299912fd842dc5afe" + integrity sha512-e/e43mQAwgYs8BY4y9l99xTK6ig1bK2uXsFLOMn9IZ16lAgulSTsotcPHVT2ZlSb/ye6Sllq7IgyDB8dGhpeXQ== + dependencies: + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-collection" "1.1.6" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-dismissable-layer" "1.1.9" + "@radix-ui/react-portal" "1.1.8" + "@radix-ui/react-presence" "1.1.4" + "@radix-ui/react-primitive" "2.1.2" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-controllable-state" "1.2.2" + "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-visually-hidden" "1.2.2" + "@radix-ui/react-toggle-group@^1.1.7": version "1.1.7" resolved "https://registry.yarnpkg.com/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.7.tgz#0eaca9e4f8fbf2536f01e33a6211eac4d6cfb83e" @@ -1194,6 +1263,13 @@ dependencies: "@radix-ui/react-use-callback-ref" "1.1.0" +"@radix-ui/react-use-escape-keydown@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz#b3fed9bbea366a118f40427ac40500aa1423cc29" + integrity sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g== + dependencies: + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-layout-effect@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27" @@ -1230,6 +1306,13 @@ dependencies: "@radix-ui/react-primitive" "2.0.2" +"@radix-ui/react-visually-hidden@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.2.tgz#aa6d0f95b0cd50f08b02393d25132f52ca7861dc" + integrity sha512-ORCmRUbNiZIv6uV5mhFrhsIKw4UX/N3syZtyqvry61tbGm4JlgQuSn0hk5TwCARsCjkcnuRkSdCE3xfb+ADHew== + dependencies: + "@radix-ui/react-primitive" "2.1.2" + "@radix-ui/rect@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.0.tgz#f817d1d3265ac5415dadc67edab30ae196696438"