Commit fff3f6c5 by Ivan

feat: tag_ids

parent c139b140
class Admin::ImagesController < ApplicationController
before_action :authorize_admin
before_action :set_image, only: [ :show, :update, :destroy, :approve, :reject, :add_tags, :remove_tag ]
before_action :set_image, only: [ :show, :edit, :update, :destroy, :approve, :reject, :add_tags, :remove_tag ]
before_action :set_tag, only: [ :index, :new, :create ], if: -> { params[:tag_id].present? }
def index
......@@ -11,7 +11,7 @@ class Admin::ImagesController < ApplicationController
render inertia: "admin/tags/images/Index", props: {
tag: @tag.as_json(only: [ :id, :name, :catalog ], methods: [ :images_count ]),
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url ]),
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url, :thumbnail_url, :medium_url ]),
filters: params[:q] || {},
pagination: {
current_page: @images.current_page,
......@@ -25,7 +25,7 @@ class Admin::ImagesController < ApplicationController
@images = @q.result(distinct: true).with_attached_file.order(created_at: :desc).page(params[:page]).per(20)
render inertia: "admin/images/Index", props: {
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url ]),
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url, :thumbnail_url, :medium_url ]),
filters: params[:q] || {},
pagination: {
current_page: @images.current_page,
......@@ -41,8 +41,15 @@ class Admin::ImagesController < ApplicationController
def show
render inertia: "admin/images/Show", props: {
image: @image.as_json(include: [ :user, :tags ], methods: [ :file_url ]),
tags: Tag.all.pluck(:name)
image: @image.as_json(include: [ :user, :tags ], methods: [ :file_url, :thumbnail_url, :medium_url ]),
tags: Tag.all.as_json
}
end
def edit
render inertia: "admin/images/Edit", props: {
image: @image.as_json(include: [ :user, :tags ], methods: [ :file_url, :thumbnail_url, :medium_url ]),
tags: Tag.all.as_json
}
end
......@@ -60,6 +67,11 @@ class Admin::ImagesController < ApplicationController
@image = Current.user.images.new(image_create_params)
if @image.save
if params[:tag_ids].present?
# 使用新的 set_tags_by_ids 方法来设置标签
@image.set_tags_by_ids(params[:tag_ids])
end
# Add the tag if we're creating from a tag context
if params[:tag_id].present? && @tag
@image.tags << @tag unless @image.tags.include?(@tag)
......@@ -86,11 +98,17 @@ class Admin::ImagesController < ApplicationController
def update
if @image.update(image_params)
# 处理标签
if params[:tag_ids].present?
# 使用新的 set_tags_by_ids 方法来设置标签
@image.set_tags_by_ids(params[:tag_ids])
end
redirect_to admin_image_path(@image), notice: "Image was successfully updated."
else
render inertia: "admin/images/Show", props: {
image: @image.as_json(include: [ :user, :tags ], methods: [ :file_url, :errors ]),
tags: Tag.all.pluck(:name),
render inertia: "admin/images/Edit", props: {
image: @image.as_json(include: [ :user, :tags ], methods: [ :file_url, :thumbnail_url, :medium_url, :errors ]),
tags: Tag.all.as_json,
errors: @image.errors
}, status: :unprocessable_entity
end
......@@ -106,7 +124,7 @@ class Admin::ImagesController < ApplicationController
@images = @q.result(distinct: true).with_attached_file.order(created_at: :desc).page(params[:page]).per(20)
render inertia: "admin/images/Pending", props: {
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url ]),
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url, :thumbnail_url, :medium_url ]),
filters: params[:q] || {},
total_count: Image.count,
pending_count: Image.pending.count,
......@@ -120,7 +138,7 @@ class Admin::ImagesController < ApplicationController
@images = @q.result(distinct: true).with_attached_file.order(created_at: :desc).page(params[:page]).per(20)
render inertia: "admin/images/Approved", props: {
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url ]),
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url, :thumbnail_url, :medium_url ]),
filters: params[:q] || {},
total_count: Image.count,
pending_count: Image.pending.count,
......@@ -134,7 +152,7 @@ class Admin::ImagesController < ApplicationController
@images = @q.result(distinct: true).with_attached_file.order(created_at: :desc).page(params[:page]).per(20)
render inertia: "admin/images/Rejected", props: {
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url ]),
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url, :thumbnail_url, :medium_url ]),
filters: params[:q] || {},
total_count: Image.count,
pending_count: Image.pending.count,
......
......@@ -9,7 +9,7 @@ class ImagesController < ApplicationController
@images = @q.result(distinct: true).with_attached_file.order(created_at: :desc).page(params[:page]).per(12)
render inertia: "images/Index", props: {
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url ]),
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url, :thumbnail_url, :medium_url ]),
filters: params[:q] || {},
pagination: {
current_page: @images.current_page,
......@@ -21,7 +21,7 @@ class ImagesController < ApplicationController
def show
render inertia: "images/Show", props: {
image: @image.as_json(include: [ :user, :tags ], methods: [ :file_url ]),
image: @image.as_json(include: [ :user, :tags ], methods: [ :file_url, :thumbnail_url, :medium_url ]),
can_edit: Current.user == @image.user && @image.status == "pending",
can_approve: Current.user&.admin?,
can_delete: Current.user == @image.user && @image.status == "pending" || Current.user&.admin?
......@@ -37,8 +37,8 @@ class ImagesController < ApplicationController
@image = Current.user.images.new(image_params)
if @image.save
if params[:tags].present?
@image.add_tags(params[:tags].split(","))
if params[:tag_ids].present?
@image.set_tags_by_ids(params[:tag_ids])
end
redirect_to image_path(@image), notice: "Image was successfully uploaded and is pending review."
else
......@@ -51,9 +51,13 @@ class ImagesController < ApplicationController
def edit
authorize_user
unless @image.status == "pending" || Current.user&.admin?
return redirect_to image_path(@image), notice: "没有权限修改"
end
render inertia: "images/Edit", props: {
image: @image.as_json(include: [ :tags ], methods: [ :file_url ]),
tags: @image.tags.pluck(:name).join(", ")
tags: Tag.all.as_json
}
end
......@@ -61,19 +65,18 @@ class ImagesController < ApplicationController
authorize_user
unless @image.status == "pending" || Current.user&.admin?
return redirect_to image_path(@image), notice: "Image was successfully updated."
return redirect_to image_path(@image), notice: "没有权限修改"
end
if @image.update(image_params)
if params[:tags].present?
@image.tags.clear
@image.add_tags(params[:tags].split(","))
if params[:tag_ids].present?
@image.set_tags_by_ids(params[:tag_ids])
end
redirect_to image_path(@image), notice: "Image was successfully updated."
else
render inertia: "images/Edit", props: {
image: @image.as_json(include: [ :tags ], methods: [ :file_url, :errors ]),
tags: params[:tags],
tags: Tag.all.as_json,
errors: @image.errors
}, status: :unprocessable_entity
end
......
import { useState, useEffect } from 'react'
import * as Popover from '@radix-ui/react-popover'
import { MagnifyingGlassIcon, XMarkIcon } from '@heroicons/react/24/outline'
import { isEqual } from 'lodash-es'
export default function TagSearchInput({ value, onChange, allTags = {}, placeholder = "输入标签名称搜索" }) {
// value 现在是逗号分隔的标签 ID 列表
const [searchQuery, setSearchQuery] = useState('')
const [selectedTags, setSelectedTags] = useState([])
const [isOpen, setIsOpen] = useState(false)
const [availableTags, setAvailableTags] = useState({})
const [filteredTags, setFilteredTags] = useState({})
const [internalValue, setInternalValue] = useState(value || [])
// 只在组件初始化或外部value变化时更新selectedTags
useEffect(() => {
if (isEqual(value, internalValue)) {
setInternalValue(value || [])
if (value) {
const tagIds = value
// 从所有可用标签中找出已选标签
const selected = []
Object.values(allTags).forEach(tagGroup => {
tagGroup.forEach(tag => {
if (tagIds.includes(tag.id)) {
selected.push(tag)
}
})
})
setSelectedTags(selected)
} else {
setSelectedTags([])
}
}
}, [value, allTags])
// 使用传入的标签数据
useEffect(() => {
setAvailableTags(allTags || {})
}, [allTags])
// 根据搜索词过滤标签
useEffect(() => {
if (!searchQuery) {
setFilteredTags(availableTags)
return
}
const query = searchQuery.toLowerCase()
const filtered = {}
Object.entries(availableTags).forEach(([catalog, tags]) => {
const matchingTags = tags.filter(tag =>
tag.name.toLowerCase().includes(query)
)
if (matchingTags.length > 0) {
filtered[catalog] = matchingTags
}
})
setFilteredTags(filtered)
}, [searchQuery, availableTags])
const handleSelectTag = (tag) => {
// 检查标签是否已经选中
const isSelected = selectedTags.some(t => t.id === tag.id)
let newSelectedTags
if (isSelected) {
// 如果已选中,则移除该标签(反选)
newSelectedTags = selectedTags.filter(t => t.id !== tag.id)
} else {
// 如果未选中,则添加该标签
newSelectedTags = [...selectedTags, tag]
}
setSelectedTags(newSelectedTags)
// 直接更新内部值并触发onChange,使用tag_ids
const newValue = newSelectedTags.map(t => t.id)
setInternalValue(newValue)
onChange(newValue)
// 不清空搜索框,保留搜索内容
}
const handleRemoveTag = (tagId) => {
const newSelectedTags = selectedTags.filter(tag => tag.id !== tagId)
setSelectedTags(newSelectedTags)
// 直接更新内部值并触发onChange,使用tag_ids
const newValue = newSelectedTags.map(t => t.id)
setInternalValue(newValue)
onChange(newValue)
}
return (
<div className="relative">
<Popover.Root open={isOpen} onOpenChange={setIsOpen} modal={false}>
<Popover.Trigger asChild>
<div className="flex flex-wrap gap-2 min-h-[2.5rem] p-2 border border-gray-300 rounded-md bg-white cursor-pointer">
{selectedTags.map(tag => (
<div
key={tag.id}
className="flex items-center bg-indigo-100 text-indigo-800 text-sm rounded-full px-3 py-1 cursor-pointer"
onClick={() => handleRemoveTag(tag.id)}
>
<span>{tag.name}</span>
<button
type="button"
onClick={(e) => {
e.stopPropagation() // 阻止事件冒泡到父元素
handleRemoveTag(tag.id)
}}
className="ml-1.5 text-indigo-600 hover:text-indigo-800 focus:outline-none"
>
<span className="sr-only">移除标签 {tag.name}</span>
<XMarkIcon className="h-3.5 w-3.5" />
</button>
</div>
))}
{selectedTags.length === 0 && (
<div className="text-gray-400 text-sm">{placeholder}</div>
)}
</div>
</Popover.Trigger>
{/* 保留搜索图标,但不作为触发器 */}
<div className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none">
<MagnifyingGlassIcon className="h-5 w-5" />
</div>
<Popover.Portal>
<Popover.Content
className="bg-white rounded-md shadow-lg p-4 w-72 max-w-[calc(100vw-2rem)] z-50"
sideOffset={5}
>
<div className="space-y-4">
<div className="relative">
<label htmlFor="tag-search" className="sr-only">
搜索标签
</label>
<input
type="text"
id="tag-search"
className="block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm pr-8"
placeholder="搜索标签..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
autoFocus
/>
{searchQuery && (
<button
type="button"
onClick={() => setSearchQuery('')}
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
aria-label="清空搜索"
>
<XMarkIcon className="h-4 w-4" />
</button>
)}
</div>
<div className="max-h-60 overflow-y-auto">
{Object.keys(filteredTags).length > 0 ? (
<div className="space-y-4">
{Object.entries(filteredTags).map(([catalog, tags]) => (
<div key={catalog || 'uncategorized'} className="space-y-2">
<h3 className="text-sm font-bold text-gray-700">
{catalog || '未分类'}
</h3>
<div className="space-y-1">
{tags.map(tag => {
const isSelected = selectedTags.some(t => t.id === tag.id)
return (
<button
key={tag.id}
type="button"
onClick={() => handleSelectTag(tag)}
// 已选中的标签也可以点击,用于反选
className={`w-full text-left px-3 py-2 text-sm rounded-md ${
isSelected
? 'bg-indigo-100 text-indigo-800'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
{tag.name}
{tag.images_count > 0 && (
<span className="ml-2 text-xs text-gray-500">
({tag.images_count})
</span>
)}
</button>
)
})}
</div>
</div>
))}
</div>
) : (
<div className="text-center py-4 text-sm text-gray-500">
{searchQuery ? '没有找到匹配的标签' : '没有可用的标签'}
</div>
)}
</div>
</div>
<Popover.Arrow className="fill-white" />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
</div>
)
}
......@@ -9,7 +9,7 @@ export function ComImageCard({ image, path, showActions = false }) {
<div className="relative pb-[75%]">
<img
loading="lazy"
src={image.file_url}
src={image.thumbnail_url || image.file_url}
alt={image.title}
className="absolute h-full w-full object-cover"
/>
......
import { useState, useEffect } from 'react'
import { useForm, usePage } from '@inertiajs/react'
import * as Form from '@radix-ui/react-form'
import TagSearchInput from '../TagSearchInput'
export default function ComImageEdit({ image, tags, path, isAdmin = false }) {
// 初始化已有标签的ID字符串表示形式
const initialTagIds = image.tags
? image.tags.map(tag => tag.id)
: []
const { data, setData, patch, processing } = useForm({
title: image.title || '',
tag_ids: initialTagIds, // 使用tag_ids而不是tags
})
// 获取所有标签数据并按目录分组
const [groupedTags, setGroupedTags] = useState({})
// 使用控制器返回的所有标签数据并按catalog属性分组
useEffect(() => {
if (Array.isArray(tags)) {
// 按catalog属性分组标签
const groupedByCategory = {}
tags.forEach(tag => {
const catalog = tag.catalog || ''
if (!groupedByCategory[catalog]) {
groupedByCategory[catalog] = []
}
groupedByCategory[catalog].push(tag)
})
setGroupedTags(groupedByCategory)
}
}, [tags])
const handleSubmit = (e) => {
e.preventDefault()
patch(path)
}
return (
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
<div className="px-4 py-5 sm:px-6">
<h1 className="text-2xl font-bold text-gray-900">编辑图片</h1>
<p className="mt-1 max-w-2xl text-sm text-gray-500">
更新图片信息
</p>
</div>
<div className="border-t border-gray-200 px-4 py-5 sm:px-6">
<div className="mb-6">
<img
src={image.file_url}
alt={image.title}
className="max-h-64 mx-auto object-contain"
/>
</div>
<Form.Root className="space-y-6" onSubmit={handleSubmit}>
<Form.Field name="title" className="space-y-2">
<Form.Label className="block text-sm font-medium text-gray-700">
标题
</Form.Label>
<Form.Control asChild>
<input
type="text"
name="title"
value={data.title}
onChange={(e) => setData('title', e.target.value)}
className="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
placeholder="输入标题"
/>
</Form.Control>
<Form.Message match="valueMissing" className="text-sm text-red-600">
请输入标题
</Form.Message>
</Form.Field>
<Form.Field name="tags" className="space-y-2">
<Form.Label className="block text-sm font-medium text-gray-700">
标签
</Form.Label>
<Form.Control asChild>
<TagSearchInput
value={data.tag_ids}
onChange={(value) => setData('tag_ids', value)}
allTags={groupedTags}
placeholder="点击搜索图标添加标签"
/>
</Form.Control>
<p className="text-xs text-gray-500">
点击右侧搜索图标可按分类浏览标签
</p>
</Form.Field>
<div className="flex justify-end space-x-3">
<a
href={path}
className="inline-flex justify-center py-2 px-4 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
取消
</a>
<Form.Submit asChild>
<button
type="submit"
disabled={processing}
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
{processing ? '保存中...' : '保存'}
</button>
</Form.Submit>
</div>
</Form.Root>
</div>
</div>
)
}
import { Head } from '@inertiajs/react'
import Layout from '../../Layout'
import ComImageEdit from '../../../components/images/ComImageEdit'
export default function AdminImagesEdit({ image, tags, auth }) {
return (
<Layout user={auth}>
<Head title={`管理 - 编辑 ${image.title}`} />
<ComImageEdit
image={image}
tags={tags}
path={`/admin/images/${image.id}`}
isAdmin={true}
/>
</Layout>
)
}
import { useState } from 'react'
import { Head, useForm } from '@inertiajs/react'
import { Head } from '@inertiajs/react'
import Layout from '../Layout'
import * as Form from '@radix-ui/react-form'
import ComImageEdit from '../../components/images/ComImageEdit'
export default function Edit({ image, tags, auth, errors = {} }) {
const { data, setData, patch, processing } = useForm({
title: image.title || '',
tags: tags || '',
})
const handleSubmit = (e) => {
e.preventDefault()
patch(`/images/${image.id}`)
}
return (
<Layout user={auth.user}>
<Head title={`Edit ${image.title}`} />
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
<div className="px-4 py-5 sm:px-6">
<h1 className="text-2xl font-bold text-gray-900">编辑图片</h1>
<p className="mt-1 max-w-2xl text-sm text-gray-500">
更新图片信息
</p>
</div>
<div className="border-t border-gray-200 px-4 py-5 sm:px-6">
<div className="mb-6">
<image
src={image.file_url}
alt={image.title}
className="max-h-64 mx-auto object-contain"
/>
</div>
<Form.Root className="space-y-6" onSubmit={handleSubmit}>
<Form.Field name="title" className="space-y-2">
<Form.Label className="block text-sm font-medium text-gray-700">
标题
</Form.Label>
<Form.Control asChild>
<input
type="text"
name="title"
value={data.title}
onChange={(e) => setData('title', e.target.value)}
className="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
placeholder="输入标题"
/>
</Form.Control>
{errors.title && (
<Form.Message className="text-sm text-red-600">
{errors.title}
</Form.Message>
)}
</Form.Field>
<Form.Field name="tags" className="space-y-2">
<Form.Label className="block text-sm font-medium text-gray-700">
标签
</Form.Label>
<Form.Control asChild>
<input
type="text"
name="tags"
value={data.tags}
onChange={(e) => setData('tags', e.target.value)}
className="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
placeholder=""
/>
</Form.Control>
<p className="text-xs text-gray-500">
{/* 请输入标签 */}
</p>
</Form.Field>
<div className="flex justify-end space-x-3">
<a
href={`/images/${image.id}`}
className="inline-flex justify-center py-2 px-4 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
取消
</a>
<Form.Submit asChild>
<button
type="submit"
disabled={processing}
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
>
{processing ? '保存中...' : '保存'}
</button>
</Form.Submit>
</div>
</Form.Root>
</div>
</div>
<ComImageEdit
image={image}
tags={tags}
path={`/images/${image.id}`}
isAdmin={false}
/>
</Layout>
)
}
......@@ -29,11 +29,41 @@ class Image < ApplicationRecord
file.attached? ? Rails.application.routes.url_helpers.rails_blob_path(file, only_path: true) : nil
end
# Method to add tags to an image
# Returns the URL for a thumbnail version of the attached file
def thumbnail_url
# return nil unless file.attached? && file.content_type.start_with?("image/")
# variant = file.variant(resize_to_limit: [300, 300]).processed
# Rails.application.routes.url_helpers.rails_blob_path(variant, only_path: true)
end
# Returns the URL for a medium-sized version of the attached file
def medium_url
# return nil unless file.attached? && file.content_type.start_with?("image/")
# variant = file.variant(resize_to_limit: [600, 600]).processed
# Rails.application.routes.url_helpers.rails_blob_path(variant, only_path: true)
end
# Method to add tags to an image by names
def add_tags(tag_names)
tag_names.each do |name|
tag = Tag.find_or_create_by(name: name.downcase.strip)
tags << tag unless tags.include?(tag)
end
end
# Method to set tags by IDs
def set_tags_by_ids(tag_ids)
return if tag_ids.nil?
# Clear existing tags if needed
tags.clear
# Add tags by ID
tag_ids.each do |id|
tag = Tag.find_by(id: id)
tags << tag if tag && !tags.include?(tag)
end
end
end
......@@ -66,7 +66,7 @@ Rails.application.routes.draw do
# get "images/add_tags", to: "images#add_tags"
# get "images/remove_tag", to: "images#remove_tag"
# get "dashboard/index", to: "dashboard#index"
resources :images, only: [ :index, :show, :update, :destroy ] do
resources :images, only: [ :index, :show, :new, :create, :edit, :update, :destroy ] do
member do
patch :approve
patch :reject
......
......@@ -13,6 +13,7 @@
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-form": "^0.1.2",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
......@@ -1270,6 +1271,43 @@
}
}
},
"node_modules/@radix-ui/react-popover": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.6.tgz",
"integrity": "sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.5",
"@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.2",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-popper": "1.2.2",
"@radix-ui/react-portal": "1.1.4",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.2",
"@radix-ui/react-slot": "1.1.2",
"@radix-ui/react-use-controllable-state": "1.1.0",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz",
......
......@@ -14,6 +14,7 @@
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-form": "^0.1.2",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment