feat: 教学设计部分 UI
This commit is contained in:
parent
3958fbc1f0
commit
92fc748a57
12
.vscode/mcp.json
vendored
Normal file
12
.vscode/mcp.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"servers": {
|
||||
"xtms - API 文档": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"apifox-mcp-server@latest",
|
||||
"--site-id=5442896"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
97
api/aifn.ts
Normal file
97
api/aifn.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import type { IResponse } from '.'
|
||||
import type { AIGeneratedContent } from '~/components/ai'
|
||||
|
||||
export type AIGeneratedContentResponse = IResponse<{
|
||||
data: AIGeneratedContent & {
|
||||
raw: string
|
||||
}
|
||||
}>
|
||||
|
||||
export const AGCStream = async <ReqT>(
|
||||
endpoint: string,
|
||||
params: ReqT,
|
||||
events: {
|
||||
onTextChunk?: (message: string) => void
|
||||
onComplete?: () => void
|
||||
}) => {
|
||||
const { onTextChunk: onMessage, onComplete } = events
|
||||
|
||||
const loginState = useLoginState()
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const baseURL = runtimeConfig.public.baseURL as string
|
||||
|
||||
const response = await fetch(new URL(endpoint, baseURL), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/event-stream',
|
||||
'Authorization': `Bearer ${loginState.token}`,
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
})
|
||||
|
||||
if (!response) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader()
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
let buffer = ''
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader?.read() || {}
|
||||
if (done) break
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
|
||||
const parts = buffer.split('\n\n')
|
||||
buffer = parts.pop()!
|
||||
|
||||
for (const part of parts) {
|
||||
if (!part.trim().startsWith('data:')) continue
|
||||
const payload = part.replace(/^data:\s*/, '').trim()
|
||||
|
||||
try {
|
||||
const obj = JSON.parse(payload)
|
||||
if (obj?.event && obj.event === 'workflow_finished') {
|
||||
onComplete?.()
|
||||
return
|
||||
}
|
||||
if (obj?.event && obj.event === 'text_chunk') {
|
||||
let text_chunk = obj.data?.text as string
|
||||
if (text_chunk) {
|
||||
if (text_chunk.startsWith('<') && text_chunk.endsWith('>')) {
|
||||
const endTag = text_chunk.match(/<\/[^>]*>/)
|
||||
if (endTag) {
|
||||
text_chunk = text_chunk.replace(endTag[0], '\n' + endTag[0])
|
||||
}
|
||||
}
|
||||
onMessage?.(text_chunk)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
onComplete?.()
|
||||
}
|
||||
|
||||
export const generateLessonPlan = async (params: {
|
||||
query: string
|
||||
}) => {
|
||||
return await http<AIGeneratedContentResponse>(`/ai/lesson-plan-design/text-block`, {
|
||||
method: 'POST',
|
||||
body: params,
|
||||
})
|
||||
}
|
||||
|
||||
export const generateCase = async (params: {
|
||||
query: string
|
||||
}) => {
|
||||
return await http<AIGeneratedContentResponse>(`/ai/case-design/text-block`, {
|
||||
method: 'POST',
|
||||
body: params,
|
||||
})
|
||||
}
|
49
components/MarkdownRenderer.vue
Normal file
49
components/MarkdownRenderer.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import md from 'markdown-it'
|
||||
import hljs from 'highlight.js'
|
||||
import 'highlight.js/styles/github-dark-dimmed.min.css'
|
||||
|
||||
const renderer = md({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
breaks: true,
|
||||
highlight: function (str, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return `<pre class="hljs" style="overflow-x: auto"><code>${
|
||||
hljs.highlight(str, { language: lang, ignoreIllegals: true }).value
|
||||
}</code></pre>`
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
'<pre class="hljs"><code>' + md().utils.escapeHtml(str) + '</code></pre>'
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
defineProps({
|
||||
source: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article
|
||||
class="prose dark:prose-invert max-w-none prose-sm prose-neutral"
|
||||
v-html="
|
||||
renderer.render(source.replaceAll('\t', ' '))
|
||||
"
|
||||
></article>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
think {
|
||||
@apply block my-4 p-3 bg-[#f0f8ff] border-l-4 border-[#88f] rounded italic text-xs;
|
||||
}
|
||||
</style>
|
129
components/ai/Conversation.vue
Normal file
129
components/ai/Conversation.vue
Normal file
@ -0,0 +1,129 @@
|
||||
<!-- eslint-disable @typescript-eslint/no-explicit-any -->
|
||||
<script lang="ts" setup>
|
||||
import type { FormContext } from 'vee-validate'
|
||||
import type { AnyZodObject } from 'zod'
|
||||
import type { LLMMessage, LLMMessages } from '.'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const props = defineProps<{
|
||||
formSchema: AnyZodObject
|
||||
form: FormContext<any>
|
||||
formFieldConfig?: {
|
||||
[key: string]: {
|
||||
component: string
|
||||
props?: Record<string, any>
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
messages?: LLMMessage[]
|
||||
messagesHistory?: LLMMessages[]
|
||||
disableUserInput?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'submit', values: FormContext<any>['values']): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col gap-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<div></div>
|
||||
<div>
|
||||
<!-- Histories -->
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<Icon name="tabler:history" />
|
||||
历史记录
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<!-- 消息区域调整 -->
|
||||
<div
|
||||
v-if="messages && messages.length > 0"
|
||||
class="flex flex-col flex-1 gap-4 max-h-[calc(100vh-280px)]"
|
||||
>
|
||||
<div
|
||||
class="flex-1 flex flex-col gap-6 overflow-y-auto scroll-smooth px-2 pb-4"
|
||||
>
|
||||
<div
|
||||
v-for="(message, i) in messages"
|
||||
:key="i"
|
||||
class="w-full flex"
|
||||
:class="`${message.role == 'user' ? 'justify-end' : 'justify-start'}`"
|
||||
>
|
||||
<div
|
||||
class="w-fit px-4 py-3 rounded-lg bg-white dark:bg-gray-800 shadow max-w-prose border"
|
||||
:class="`${message.role == 'user' ? 'rounded-br-none' : 'rounded-bl-none'}`"
|
||||
>
|
||||
<MarkdownRenderer
|
||||
v-if="!!message.content"
|
||||
:source="message.content"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center gap-2 text-foreground/60 text-sm font-medium"
|
||||
>
|
||||
<Icon
|
||||
name="svg-spinners:270-ring-with-bg"
|
||||
class="text-lg"
|
||||
/>
|
||||
思考中
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!disableUserInput"
|
||||
class="relative"
|
||||
>
|
||||
<Textarea
|
||||
class="w-full h-12 pr-24 shadow-lg"
|
||||
placeholder="请告诉我额外的补充需求,我会尽量满足您的要求"
|
||||
/>
|
||||
<div class="absolute right-2 bottom-2">
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
>
|
||||
<Icon name="tabler:send" />
|
||||
发送
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表单区域不变 -->
|
||||
<div
|
||||
v-else
|
||||
class="rounded-lg p-6 bg-primary/5"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<AutoForm
|
||||
v-if="formSchema && form"
|
||||
:schema="formSchema"
|
||||
:form="form"
|
||||
:field-config="formFieldConfig"
|
||||
class="space-y-2"
|
||||
@submit="(values) => $emit('submit', values)"
|
||||
>
|
||||
<div class="w-full flex justify-center gap-2 pt-4">
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
>
|
||||
<Icon name="mage:stars-c-fill" />
|
||||
一键生成
|
||||
</Button>
|
||||
</div>
|
||||
</AutoForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
110
components/ai/GeneratedContent.vue
Normal file
110
components/ai/GeneratedContent.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<script lang="ts" setup>
|
||||
import type { AIGeneratedContent } from '.'
|
||||
|
||||
defineProps<{
|
||||
data?: AIGeneratedContent | null
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'regenerate' | 'delete', itemIndex: number): void
|
||||
(e: 'regenerateAll' | 'download' | 'save'): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="data"
|
||||
class="flex flex-col gap-4 rounded-md border p-4 overflow-hidden relative"
|
||||
>
|
||||
<!-- header -->
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon
|
||||
name="mage:stars-c-fill"
|
||||
class="text-xl text-amber-500"
|
||||
/>
|
||||
<h1 class="text-lg font-medium">
|
||||
{{ data.title }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="$emit('regenerateAll')"
|
||||
>
|
||||
<Icon name="tabler:reload" />
|
||||
重新生成
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="$emit('download')"
|
||||
>
|
||||
<Icon name="tabler:download" />
|
||||
下载
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@click="$emit('save')"
|
||||
>
|
||||
<Icon name="tabler:copy" />
|
||||
存入教案库
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- items -->
|
||||
<div
|
||||
v-for="(item, i) in data.sections"
|
||||
:key="i"
|
||||
class="flex flex-col rounded-md border overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="flex justify-between items-center gap-2 p-2 px-4 bg-gradient-to-r from-primary/15 to-muted text-background"
|
||||
>
|
||||
<span class="text-sm text-foreground font-medium">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
<div class="flex items-center">
|
||||
<!-- <Button
|
||||
variant="link"
|
||||
size="xs"
|
||||
class="text-muted-foreground"
|
||||
@click="$emit('regenerate', i)"
|
||||
>
|
||||
<Icon name="tabler:reload" />
|
||||
重新生成
|
||||
</Button> -->
|
||||
<Button
|
||||
variant="link"
|
||||
size="xs"
|
||||
class="text-muted-foreground"
|
||||
@click="$emit('delete', i)"
|
||||
>
|
||||
<Icon name="tabler:trash" />
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 py-2 bg-background">
|
||||
<MarkdownRenderer :source="item.content" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot
|
||||
v-else
|
||||
name="empty"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col justify-center items-center gap-2 py-8 rounded-md border"
|
||||
>
|
||||
<Icon
|
||||
name="lucide:text-cursor-input"
|
||||
class="text-3xl opacity-50"
|
||||
/>
|
||||
<span class="text-xs font-medium opacity-50">等待生成内容</span>
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
21
components/ai/index.ts
Normal file
21
components/ai/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export interface AIGeneratedContent {
|
||||
title: string
|
||||
sections: AIGeneratedContentItem[]
|
||||
}
|
||||
|
||||
export interface AIGeneratedContentItem {
|
||||
title: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface LLMMessage {
|
||||
role: 'user' | 'assistant' | 'system'
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface LLMMessages {
|
||||
id?: string | number
|
||||
timestamp: number
|
||||
title: string
|
||||
messages: LLMMessage[]
|
||||
}
|
@ -18,7 +18,7 @@ defineProps<{
|
||||
/>
|
||||
</slot>
|
||||
<div
|
||||
class="h-full rounded-lg shadow-sm overflow-hidden"
|
||||
class="h-full rounded-lg shadow-sm overflow-hidden relative"
|
||||
:class="twMerge('bg-white dark:bg-neutral-900 p-8 z-20', contentClass)"
|
||||
>
|
||||
<slot />
|
||||
|
111
components/fn/teach/CaseGen.vue
Normal file
111
components/fn/teach/CaseGen.vue
Normal file
@ -0,0 +1,111 @@
|
||||
<script lang="ts" setup>
|
||||
import { toast } from 'vue-sonner'
|
||||
import { generateCase, type AIGeneratedContentResponse } from '~/api/aifn'
|
||||
import type { AIGeneratedContent } from '~/components/ai'
|
||||
import type { FetchError } from '~/types'
|
||||
|
||||
const tab = ref('text')
|
||||
const loading = ref(false)
|
||||
|
||||
const input = ref('')
|
||||
|
||||
const data = ref<AIGeneratedContent | null>(null)
|
||||
|
||||
const onGenerateClick = () => {
|
||||
if (input.value) {
|
||||
loading.value = true
|
||||
toast.promise(
|
||||
generateCase({
|
||||
query: input.value,
|
||||
}),
|
||||
{
|
||||
loading: '生成中...',
|
||||
success: (res: AIGeneratedContentResponse) => {
|
||||
data.value = res.data
|
||||
return '生成成功'
|
||||
},
|
||||
error: (err: FetchError) => {
|
||||
data.value = null
|
||||
return err.message
|
||||
},
|
||||
finally: () => {
|
||||
loading.value = false
|
||||
},
|
||||
},
|
||||
)
|
||||
} else {
|
||||
data.value = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<Tabs
|
||||
v-model="tab"
|
||||
class="w-[400px]"
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="text">
|
||||
<div class="flex items-center gap-1">
|
||||
<Icon name="tabler:article" />
|
||||
<span>文本生成</span>
|
||||
</div>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="chapter">
|
||||
<div class="flex items-center gap-1">
|
||||
<Icon name="tabler:text-plus" />
|
||||
<span>章节生成</span>
|
||||
</div>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="bot"
|
||||
disabled
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<Icon name="tabler:robot" />
|
||||
<span>课程智能体</span>
|
||||
</div>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
v-if="tab === 'chapter'"
|
||||
class="flex h-20 flex-col justify-center items-center gap-1 px-8 rounded-md border"
|
||||
>
|
||||
<Icon
|
||||
name="tabler:text-plus"
|
||||
class="text-3xl"
|
||||
/>
|
||||
<span class="text-xs font-medium">选择章节</span>
|
||||
</div>
|
||||
<Textarea
|
||||
v-model="input"
|
||||
placeholder="请输入文本来生成内容"
|
||||
class="h-20 flex-1"
|
||||
/>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<Button
|
||||
size="lg"
|
||||
:disabled="loading"
|
||||
@click="onGenerateClick"
|
||||
>
|
||||
<Icon
|
||||
v-if="loading"
|
||||
name="svg-spinners:180-ring-with-bg"
|
||||
/>
|
||||
<Icon
|
||||
v-else
|
||||
name="mage:stars-c-fill"
|
||||
/>
|
||||
生成案例
|
||||
</Button>
|
||||
<p class="text-xs text-foreground/40">内容由 AI 生成,仅供参考</p>
|
||||
</div>
|
||||
</div>
|
||||
<AiGeneratedContent :data />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
25
components/fn/teach/KnowledgeDiagram.vue
Normal file
25
components/fn/teach/KnowledgeDiagram.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<script lang="ts" setup>
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { z } from 'zod'
|
||||
|
||||
const schema = z.object({
|
||||
foo: z.string().describe('测试Label').optional(),
|
||||
bar: z.number(),
|
||||
})
|
||||
|
||||
const form = useForm({
|
||||
validationSchema: toTypedSchema(schema),
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<AiConversation
|
||||
:form
|
||||
:form-schema="schema"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -1,5 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
import { toast } from 'vue-sonner'
|
||||
import { generateLessonPlan, type AIGeneratedContentResponse } from '~/api/aifn'
|
||||
import type { AIGeneratedContent } from '~/components/ai'
|
||||
import type { FetchError } from '~/types'
|
||||
|
||||
const tab = ref('text')
|
||||
const loading = ref(false)
|
||||
|
||||
const input = ref('')
|
||||
|
||||
const data = ref<AIGeneratedContent | null>(null)
|
||||
|
||||
const onGenerateClick = () => {
|
||||
if (input.value) {
|
||||
loading.value = true
|
||||
toast.promise(
|
||||
generateLessonPlan({
|
||||
query: input.value,
|
||||
}),
|
||||
{
|
||||
loading: '生成中...',
|
||||
success: (res: AIGeneratedContentResponse) => {
|
||||
data.value = res.data
|
||||
return '生成成功'
|
||||
},
|
||||
error: (err: FetchError) => {
|
||||
data.value = null
|
||||
return err.message
|
||||
},
|
||||
finally: () => {
|
||||
loading.value = false
|
||||
},
|
||||
},
|
||||
)
|
||||
} else {
|
||||
data.value = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -44,117 +81,31 @@ const tab = ref('text')
|
||||
<span class="text-xs font-medium">选择章节</span>
|
||||
</div>
|
||||
<Textarea
|
||||
v-model="input"
|
||||
placeholder="请输入文本来生成内容"
|
||||
class="h-20 flex-1"
|
||||
/>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<Button size="lg">
|
||||
<Icon name="mage:stars-c-fill" />
|
||||
<Button
|
||||
size="lg"
|
||||
:disabled="loading"
|
||||
@click="onGenerateClick"
|
||||
>
|
||||
<Icon
|
||||
v-if="loading"
|
||||
name="svg-spinners:180-ring-with-bg"
|
||||
/>
|
||||
<Icon
|
||||
v-else
|
||||
name="mage:stars-c-fill"
|
||||
/>
|
||||
生成教案
|
||||
</Button>
|
||||
<p class="text-xs text-foreground/40">内容由 AI 生成,仅供参考</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col gap-4 rounded-md border p-4 overflow-hidden relative"
|
||||
>
|
||||
<!-- <div class="absolute inset-0 -z-10 overflow-hidden rounded-md opacity-30">
|
||||
<div
|
||||
class="bubble animate-bubble -top-24 -left-14 size-48"
|
||||
style="--i: 0"
|
||||
/>
|
||||
<div
|
||||
class="bubble animate-bubble -top-10 -right-8 size-28"
|
||||
style="--i: 1"
|
||||
/>
|
||||
</div> -->
|
||||
<!-- header -->
|
||||
<div class="flex justify-between items-start gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon
|
||||
name="mage:stars-c-fill"
|
||||
class="text-xl text-amber-500"
|
||||
/>
|
||||
<h1 class="text-lg font-medium">生成的教案标题</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<Icon name="tabler:reload" />
|
||||
重新生成
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<Icon name="tabler:download" />
|
||||
下载
|
||||
</Button>
|
||||
<Button size="sm">
|
||||
<Icon name="tabler:copy" />
|
||||
存入教案库
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- items -->
|
||||
<div class="flex flex-col rounded-md border overflow-hidden">
|
||||
<div class="flex justify-between items-center gap-2 p-2 px-4 bg-gradient-to-r from-primary/15 to-muted text-background">
|
||||
<span class="text-sm text-foreground font-medium">教学目标</span>
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
variant="link"
|
||||
size="xs"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
<Icon name="tabler:reload" />
|
||||
重新生成
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
size="xs"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
<Icon name="tabler:trash" />
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 py-2 prose prose-sm bg-background">
|
||||
<ol>
|
||||
<li>教学目标 1</li>
|
||||
<li>教学目标 2</li>
|
||||
<li>教学目标 3</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AiGeneratedContent :data />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bubble {
|
||||
@apply absolute bg-gradient-to-tr from-primary to-secondary rounded-full shadow-lg;
|
||||
--i: 0;
|
||||
--animation-delay: calc(var(--i) * 0.5s);
|
||||
}
|
||||
|
||||
.animate-bubble {
|
||||
animation: bubble 20s infinite;
|
||||
animation-delay: var(--animation-delay);
|
||||
}
|
||||
|
||||
/* bubble jump slowly */
|
||||
@keyframes bubble {
|
||||
0% {
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px) scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
56
components/fn/teach/StdDesign.vue
Normal file
56
components/fn/teach/StdDesign.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<script lang="ts" setup>
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { z } from 'zod'
|
||||
import { AGCStream } from '~/api/aifn'
|
||||
import type { LLMMessage } from '~/components/ai'
|
||||
|
||||
const messages = ref<LLMMessage[]>([])
|
||||
|
||||
const schema = z.object({
|
||||
query: z.string().describe('课程名称'),
|
||||
})
|
||||
|
||||
const form = useForm({
|
||||
validationSchema: toTypedSchema(schema),
|
||||
})
|
||||
|
||||
const onSubmit = (values: z.infer<typeof schema>) => {
|
||||
// const valuesNoNil = useOmitBy(values, (v) => v == null || v === '')
|
||||
messages.value.push({
|
||||
role: 'user',
|
||||
content: `*生成一份 ${values.query} 的课程标准*`,
|
||||
})
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
})
|
||||
AGCStream(
|
||||
'/ai/course-standard/stream',
|
||||
{ query: values.query },
|
||||
{
|
||||
onTextChunk: (chunk) => {
|
||||
console.log(chunk)
|
||||
messages.value[messages.value.length - 1].content += chunk
|
||||
},
|
||||
onComplete: () => {
|
||||
console.log('complete')
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<AiConversation
|
||||
:form
|
||||
:form-schema="schema"
|
||||
:messages="messages"
|
||||
disable-user-input
|
||||
@submit="onSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
@ -3,6 +3,7 @@ export interface NavTertiaryItem {
|
||||
label: string
|
||||
to?: string
|
||||
component?: string | Component
|
||||
props?: Record<string, unknown>
|
||||
}
|
||||
</script>
|
||||
|
||||
|
19
components/ui/accordion/Accordion.vue
Normal file
19
components/ui/accordion/Accordion.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
AccordionRoot,
|
||||
type AccordionRootEmits,
|
||||
type AccordionRootProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
|
||||
const props = defineProps<AccordionRootProps>()
|
||||
const emits = defineEmits<AccordionRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</AccordionRoot>
|
||||
</template>
|
24
components/ui/accordion/AccordionContent.vue
Normal file
24
components/ui/accordion/AccordionContent.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import { AccordionContent, type AccordionContentProps } from 'reka-ui'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<AccordionContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionContent
|
||||
v-bind="delegatedProps"
|
||||
class="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
>
|
||||
<div :class="cn('pb-4 pt-0', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</template>
|
24
components/ui/accordion/AccordionItem.vue
Normal file
24
components/ui/accordion/AccordionItem.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import { AccordionItem, type AccordionItemProps, useForwardProps } from 'reka-ui'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionItem
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('border-b', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</AccordionItem>
|
||||
</template>
|
39
components/ui/accordion/AccordionTrigger.vue
Normal file
39
components/ui/accordion/AccordionTrigger.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ChevronDown } from 'lucide-vue-next'
|
||||
import {
|
||||
AccordionHeader,
|
||||
AccordionTrigger,
|
||||
type AccordionTriggerProps,
|
||||
} from 'reka-ui'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionHeader class="flex">
|
||||
<AccordionTrigger
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
<slot name="icon">
|
||||
<ChevronDown
|
||||
class="h-4 w-4 shrink-0 transition-transform duration-200"
|
||||
/>
|
||||
</slot>
|
||||
</AccordionTrigger>
|
||||
</AccordionHeader>
|
||||
</template>
|
4
components/ui/accordion/index.ts
Normal file
4
components/ui/accordion/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export { default as Accordion } from './Accordion.vue'
|
||||
export { default as AccordionContent } from './AccordionContent.vue'
|
||||
export { default as AccordionItem } from './AccordionItem.vue'
|
||||
export { default as AccordionTrigger } from './AccordionTrigger.vue'
|
105
components/ui/auto-form/AutoForm.vue
Normal file
105
components/ui/auto-form/AutoForm.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<script setup lang="ts" generic="T extends ZodObjectOrWrapped">
|
||||
import type { FormContext, GenericObject } from 'vee-validate'
|
||||
import type { z, ZodAny } from 'zod'
|
||||
import type { Config, ConfigItem, Dependency, Shape } from './interface'
|
||||
import { Form } from '@/components/ui/form'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { computed, toRefs } from 'vue'
|
||||
import AutoFormField from './AutoFormField.vue'
|
||||
import { provideDependencies } from './dependencies'
|
||||
import { getBaseSchema, getBaseType, getDefaultValueInZodStack, getObjectFormSchema, type ZodObjectOrWrapped } from './utils'
|
||||
|
||||
const props = defineProps<{
|
||||
schema: T
|
||||
form?: FormContext<GenericObject>
|
||||
fieldConfig?: Config<z.infer<T>>
|
||||
dependencies?: Dependency<z.infer<T>>[]
|
||||
}>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
submit: [event: z.infer<T>]
|
||||
}>()
|
||||
|
||||
const { dependencies } = toRefs(props)
|
||||
provideDependencies(dependencies)
|
||||
|
||||
const shapes = computed(() => {
|
||||
// @ts-expect-error ignore {} not assignable to object
|
||||
const val: { [key in keyof T]: Shape } = {}
|
||||
const baseSchema = getObjectFormSchema(props.schema)
|
||||
const shape = baseSchema.shape
|
||||
Object.keys(shape).forEach((name) => {
|
||||
const item = shape[name] as ZodAny
|
||||
const baseItem = getBaseSchema(item) as ZodAny
|
||||
let options = (baseItem && 'values' in baseItem._def) ? baseItem._def.values as string[] : undefined
|
||||
if (!Array.isArray(options) && typeof options === 'object')
|
||||
options = Object.values(options)
|
||||
|
||||
val[name as keyof T] = {
|
||||
type: getBaseType(item),
|
||||
default: getDefaultValueInZodStack(item),
|
||||
options,
|
||||
required: !['ZodOptional', 'ZodNullable'].includes(item._def.typeName),
|
||||
schema: baseItem,
|
||||
}
|
||||
})
|
||||
return val
|
||||
})
|
||||
|
||||
const fields = computed(() => {
|
||||
// @ts-expect-error ignore {} not assignable to object
|
||||
const val: { [key in keyof z.infer<T>]: { shape: Shape, fieldName: string, config: ConfigItem } } = {}
|
||||
for (const key in shapes.value) {
|
||||
const shape = shapes.value[key]
|
||||
val[key as keyof z.infer<T>] = {
|
||||
shape,
|
||||
config: props.fieldConfig?.[key] as ConfigItem,
|
||||
fieldName: key,
|
||||
}
|
||||
}
|
||||
return val
|
||||
})
|
||||
|
||||
const formComponent = computed(() => props.form ? 'form' : Form)
|
||||
const formComponentProps = computed(() => {
|
||||
if (props.form) {
|
||||
return {
|
||||
onSubmit: props.form.handleSubmit(val => emits('submit', val)),
|
||||
};
|
||||
}
|
||||
else {
|
||||
const formSchema = toTypedSchema(props.schema)
|
||||
return {
|
||||
keepValues: true,
|
||||
validationSchema: formSchema,
|
||||
onSubmit: (val: GenericObject) => emits('submit', val),
|
||||
};
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="formComponent"
|
||||
v-bind="formComponentProps"
|
||||
>
|
||||
<slot name="customAutoForm" :fields="fields">
|
||||
<template v-for="(shape, key) of shapes" :key="key">
|
||||
<slot
|
||||
:shape="shape"
|
||||
:name="key.toString() as keyof z.infer<T>"
|
||||
:field-name="key.toString()"
|
||||
:config="fieldConfig?.[key as keyof typeof fieldConfig] as ConfigItem"
|
||||
>
|
||||
<AutoFormField
|
||||
:config="fieldConfig?.[key as keyof typeof fieldConfig] as ConfigItem"
|
||||
:field-name="key.toString()"
|
||||
:shape="shape"
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
</slot>
|
||||
|
||||
<slot :shapes="shapes" />
|
||||
</component>
|
||||
</template>
|
45
components/ui/auto-form/AutoFormField.vue
Normal file
45
components/ui/auto-form/AutoFormField.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<script setup lang="ts" generic="U extends ZodAny">
|
||||
import type { ZodAny } from 'zod'
|
||||
import type { Config, ConfigItem, Shape } from './interface'
|
||||
import { computed } from 'vue'
|
||||
import { DEFAULT_ZOD_HANDLERS, INPUT_COMPONENTS } from './constant'
|
||||
import useDependencies from './dependencies'
|
||||
|
||||
const props = defineProps<{
|
||||
fieldName: string
|
||||
shape: Shape
|
||||
config?: ConfigItem | Config<U>
|
||||
}>()
|
||||
|
||||
function isValidConfig(config: any): config is ConfigItem {
|
||||
return !!config?.component
|
||||
}
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
if (['ZodObject', 'ZodArray'].includes(props.shape?.type))
|
||||
return { schema: props.shape?.schema }
|
||||
return undefined
|
||||
})
|
||||
|
||||
const { isDisabled, isHidden, isRequired, overrideOptions } = useDependencies(props.fieldName)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="isValidConfig(config)
|
||||
? typeof config.component === 'string'
|
||||
? INPUT_COMPONENTS[config.component!]
|
||||
: config.component
|
||||
: INPUT_COMPONENTS[DEFAULT_ZOD_HANDLERS[shape.type]] "
|
||||
v-if="!isHidden"
|
||||
:field-name="fieldName"
|
||||
:label="shape.schema?.description"
|
||||
:required="isRequired || shape.required"
|
||||
:options="overrideOptions || shape.options"
|
||||
:disabled="isDisabled"
|
||||
:config="config"
|
||||
v-bind="delegatedProps"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
110
components/ui/auto-form/AutoFormFieldArray.vue
Normal file
110
components/ui/auto-form/AutoFormFieldArray.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<script setup lang="ts" generic="T extends z.ZodAny">
|
||||
import type { Config, ConfigItem } from './interface'
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { FormItem, FormMessage } from '@/components/ui/form'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { PlusIcon, TrashIcon } from 'lucide-vue-next'
|
||||
import { FieldArray, FieldContextKey, useField } from 'vee-validate'
|
||||
import { computed, provide } from 'vue'
|
||||
import * as z from 'zod'
|
||||
import AutoFormField from './AutoFormField.vue'
|
||||
import AutoFormLabel from './AutoFormLabel.vue'
|
||||
import { beautifyObjectName, getBaseType } from './utils'
|
||||
|
||||
const props = defineProps<{
|
||||
fieldName: string
|
||||
required?: boolean
|
||||
config?: Config<T>
|
||||
schema?: z.ZodArray<T>
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
function isZodArray(
|
||||
item: z.ZodArray<any> | z.ZodDefault<any>,
|
||||
): item is z.ZodArray<any> {
|
||||
return item instanceof z.ZodArray
|
||||
}
|
||||
|
||||
function isZodDefault(
|
||||
item: z.ZodArray<any> | z.ZodDefault<any>,
|
||||
): item is z.ZodDefault<any> {
|
||||
return item instanceof z.ZodDefault
|
||||
}
|
||||
|
||||
const itemShape = computed(() => {
|
||||
if (!props.schema)
|
||||
return
|
||||
|
||||
const schema: z.ZodAny = isZodArray(props.schema)
|
||||
? props.schema._def.type
|
||||
: isZodDefault(props.schema)
|
||||
// @ts-expect-error missing schema
|
||||
? props.schema._def.innerType._def.type
|
||||
: null
|
||||
|
||||
return {
|
||||
type: getBaseType(schema),
|
||||
schema,
|
||||
}
|
||||
})
|
||||
|
||||
const fieldContext = useField(props.fieldName)
|
||||
// @ts-expect-error ignore missing `id`
|
||||
provide(FieldContextKey, fieldContext)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FieldArray v-slot="{ fields, remove, push }" as="section" :name="fieldName">
|
||||
<slot v-bind="props">
|
||||
<Accordion type="multiple" class="w-full" collapsible :disabled="disabled" as-child>
|
||||
<FormItem>
|
||||
<AccordionItem :value="fieldName" class="border-none">
|
||||
<AccordionTrigger>
|
||||
<AutoFormLabel class="text-base" :required="required">
|
||||
{{ schema?.description || beautifyObjectName(fieldName) }}
|
||||
</AutoFormLabel>
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent>
|
||||
<template v-for="(field, index) of fields" :key="field.key">
|
||||
<div class="mb-4 p-1">
|
||||
<AutoFormField
|
||||
:field-name="`${fieldName}[${index}]`"
|
||||
:label="fieldName"
|
||||
:shape="itemShape!"
|
||||
:config="config as ConfigItem"
|
||||
/>
|
||||
|
||||
<div class="!my-4 flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="secondary"
|
||||
@click="remove(index)"
|
||||
>
|
||||
<TrashIcon :size="16" />
|
||||
</Button>
|
||||
</div>
|
||||
<Separator v-if="!field.isLast" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
class="mt-4 flex items-center"
|
||||
@click="push(null)"
|
||||
>
|
||||
<PlusIcon class="mr-2" :size="16" />
|
||||
Add
|
||||
</Button>
|
||||
</AccordionContent>
|
||||
|
||||
<FormMessage />
|
||||
</AccordionItem>
|
||||
</FormItem>
|
||||
</Accordion>
|
||||
</slot>
|
||||
</FieldArray>
|
||||
</template>
|
41
components/ui/auto-form/AutoFormFieldBoolean.vue
Normal file
41
components/ui/auto-form/AutoFormFieldBoolean.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import type { FieldProps } from './interface'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '@/components/ui/form'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { computed } from 'vue'
|
||||
import AutoFormLabel from './AutoFormLabel.vue'
|
||||
import { beautifyObjectName, maybeBooleanishToBoolean } from './utils'
|
||||
|
||||
const props = defineProps<FieldProps>()
|
||||
|
||||
const booleanComponent = computed(() => props.config?.component === 'switch' ? Switch : Checkbox)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormField v-slot="slotProps" :name="fieldName">
|
||||
<FormItem>
|
||||
<div class="space-y-0 mb-3 flex items-center gap-3">
|
||||
<FormControl>
|
||||
<slot v-bind="slotProps">
|
||||
<component
|
||||
:is="booleanComponent"
|
||||
:disabled="maybeBooleanishToBoolean(config?.inputProps?.disabled) ?? disabled"
|
||||
:name="slotProps.componentField.name"
|
||||
:model-value="slotProps.componentField.modelValue"
|
||||
@update:model-value="slotProps.componentField['onUpdate:modelValue']"
|
||||
/>
|
||||
</slot>
|
||||
</FormControl>
|
||||
<AutoFormLabel v-if="!config?.hideLabel" :required="required">
|
||||
{{ config?.label || beautifyObjectName(label ?? fieldName) }}
|
||||
</AutoFormLabel>
|
||||
</div>
|
||||
|
||||
<FormDescription v-if="config?.description">
|
||||
{{ config.description }}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</template>
|
57
components/ui/auto-form/AutoFormFieldDate.vue
Normal file
57
components/ui/auto-form/AutoFormFieldDate.vue
Normal file
@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import type { FieldProps } from './interface'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Calendar } from '@/components/ui/calendar'
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '@/components/ui/form'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
|
||||
import { DateFormatter, getLocalTimeZone } from '@internationalized/date'
|
||||
import { CalendarIcon } from 'lucide-vue-next'
|
||||
import AutoFormLabel from './AutoFormLabel.vue'
|
||||
import { beautifyObjectName, maybeBooleanishToBoolean } from './utils'
|
||||
|
||||
defineProps<FieldProps>()
|
||||
|
||||
const df = new DateFormatter('en-US', {
|
||||
dateStyle: 'long',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormField v-slot="slotProps" :name="fieldName">
|
||||
<FormItem>
|
||||
<AutoFormLabel v-if="!config?.hideLabel" :required="required">
|
||||
{{ config?.label || beautifyObjectName(label ?? fieldName) }}
|
||||
</AutoFormLabel>
|
||||
<FormControl>
|
||||
<slot v-bind="slotProps">
|
||||
<div>
|
||||
<Popover>
|
||||
<PopoverTrigger as-child :disabled="maybeBooleanishToBoolean(config?.inputProps?.disabled) ?? disabled">
|
||||
<Button
|
||||
variant="outline"
|
||||
:class="cn(
|
||||
'w-full justify-start text-left font-normal',
|
||||
!slotProps.componentField.modelValue && 'text-muted-foreground',
|
||||
)"
|
||||
>
|
||||
<CalendarIcon class="mr-2 h-4 w-4" :size="16" />
|
||||
{{ slotProps.componentField.modelValue ? df.format(slotProps.componentField.modelValue.toDate(getLocalTimeZone())) : "Pick a date" }}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-auto p-0">
|
||||
<Calendar initial-focus v-bind="slotProps.componentField" />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</slot>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription v-if="config?.description">
|
||||
{{ config.description }}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</template>
|
49
components/ui/auto-form/AutoFormFieldEnum.vue
Normal file
49
components/ui/auto-form/AutoFormFieldEnum.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import type { FieldProps } from './interface'
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '@/components/ui/form'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import AutoFormLabel from './AutoFormLabel.vue'
|
||||
import { beautifyObjectName, maybeBooleanishToBoolean } from './utils'
|
||||
|
||||
defineProps<FieldProps & {
|
||||
options?: string[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormField v-slot="slotProps" :name="fieldName">
|
||||
<FormItem>
|
||||
<AutoFormLabel v-if="!config?.hideLabel" :required="required">
|
||||
{{ config?.label || beautifyObjectName(label ?? fieldName) }}
|
||||
</AutoFormLabel>
|
||||
<FormControl>
|
||||
<slot v-bind="slotProps">
|
||||
<RadioGroup v-if="config?.component === 'radio'" :disabled="maybeBooleanishToBoolean(config?.inputProps?.disabled) ?? disabled" :orientation="'vertical'" v-bind="{ ...slotProps.componentField }">
|
||||
<div v-for="(option, index) in options" :key="option" class="mb-2 flex items-center gap-3 space-y-0">
|
||||
<RadioGroupItem :id="`${option}-${index}`" :value="option" />
|
||||
<Label :for="`${option}-${index}`">{{ beautifyObjectName(option) }}</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
|
||||
<Select v-else :disabled="maybeBooleanishToBoolean(config?.inputProps?.disabled) ?? disabled" v-bind="{ ...slotProps.componentField }">
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue :placeholder="config?.inputProps?.placeholder" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="option in options" :key="option" :value="option">
|
||||
{{ beautifyObjectName(option) }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</slot>
|
||||
</FormControl>
|
||||
|
||||
<FormDescription v-if="config?.description">
|
||||
{{ config.description }}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</template>
|
74
components/ui/auto-form/AutoFormFieldFile.vue
Normal file
74
components/ui/auto-form/AutoFormFieldFile.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import type { FieldProps } from './interface'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { TrashIcon } from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
import AutoFormLabel from './AutoFormLabel.vue'
|
||||
import { beautifyObjectName } from './utils'
|
||||
|
||||
defineProps<FieldProps>()
|
||||
|
||||
const inputFile = ref<File>()
|
||||
async function parseFileAsString(file: File | undefined): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (file) {
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = () => {
|
||||
resolve(reader.result as string)
|
||||
}
|
||||
reader.onerror = (err) => {
|
||||
reject(err)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormField v-slot="slotProps" :name="fieldName">
|
||||
<FormItem v-bind="$attrs">
|
||||
<AutoFormLabel v-if="!config?.hideLabel" :required="required">
|
||||
{{ config?.label || beautifyObjectName(label ?? fieldName) }}
|
||||
</AutoFormLabel>
|
||||
<FormControl>
|
||||
<slot v-bind="slotProps">
|
||||
<Input
|
||||
v-if="!inputFile"
|
||||
type="file"
|
||||
v-bind="{ ...config?.inputProps }"
|
||||
:disabled="config?.inputProps?.disabled ?? disabled"
|
||||
@change="async (ev: InputEvent) => {
|
||||
const file = (ev.target as HTMLInputElement).files?.[0]
|
||||
inputFile = file
|
||||
const parsed = await parseFileAsString(file)
|
||||
slotProps.componentField.onInput(parsed)
|
||||
}"
|
||||
/>
|
||||
<div v-else class="flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent pl-3 pr-1 py-1 text-sm shadow-sm transition-colors">
|
||||
<p>{{ inputFile?.name }}</p>
|
||||
<Button
|
||||
:size="'icon'"
|
||||
:variant="'ghost'"
|
||||
class="h-[26px] w-[26px]"
|
||||
aria-label="Remove file"
|
||||
type="button"
|
||||
@click="() => {
|
||||
inputFile = undefined
|
||||
slotProps.componentField.onInput(undefined)
|
||||
}"
|
||||
>
|
||||
<TrashIcon :size="16" />
|
||||
</Button>
|
||||
</div>
|
||||
</slot>
|
||||
</FormControl>
|
||||
<FormDescription v-if="config?.description">
|
||||
{{ config.description }}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</template>
|
36
components/ui/auto-form/AutoFormFieldInput.vue
Normal file
36
components/ui/auto-form/AutoFormFieldInput.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import type { FieldProps } from './interface'
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { computed } from 'vue'
|
||||
import AutoFormLabel from './AutoFormLabel.vue'
|
||||
import { beautifyObjectName } from './utils'
|
||||
|
||||
const props = defineProps<FieldProps>()
|
||||
const inputComponent = computed(() => props.config?.component === 'textarea' ? Textarea : Input)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormField v-slot="slotProps" :name="fieldName">
|
||||
<FormItem v-bind="$attrs">
|
||||
<AutoFormLabel v-if="!config?.hideLabel" :required="required">
|
||||
{{ config?.label || beautifyObjectName(label ?? fieldName) }}
|
||||
</AutoFormLabel>
|
||||
<FormControl>
|
||||
<slot v-bind="slotProps">
|
||||
<component
|
||||
:is="inputComponent"
|
||||
type="text"
|
||||
v-bind="{ ...slotProps.componentField, ...config?.inputProps }"
|
||||
:disabled="config?.inputProps?.disabled ?? disabled"
|
||||
/>
|
||||
</slot>
|
||||
</FormControl>
|
||||
<FormDescription v-if="config?.description">
|
||||
{{ config.description }}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</template>
|
32
components/ui/auto-form/AutoFormFieldNumber.vue
Normal file
32
components/ui/auto-form/AutoFormFieldNumber.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { FieldProps } from './interface'
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormMessage } from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import AutoFormLabel from './AutoFormLabel.vue'
|
||||
import { beautifyObjectName } from './utils'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
defineProps<FieldProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormField v-slot="slotProps" :name="fieldName">
|
||||
<FormItem>
|
||||
<AutoFormLabel v-if="!config?.hideLabel" :required="required">
|
||||
{{ config?.label || beautifyObjectName(label ?? fieldName) }}
|
||||
</AutoFormLabel>
|
||||
<FormControl>
|
||||
<slot v-bind="slotProps">
|
||||
<Input type="number" v-bind="{ ...slotProps.componentField, ...config?.inputProps }" :disabled="config?.inputProps?.disabled ?? disabled" />
|
||||
</slot>
|
||||
</FormControl>
|
||||
<FormDescription v-if="config?.description">
|
||||
{{ config.description }}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</template>
|
78
components/ui/auto-form/AutoFormFieldObject.vue
Normal file
78
components/ui/auto-form/AutoFormFieldObject.vue
Normal file
@ -0,0 +1,78 @@
|
||||
<script setup lang="ts" generic="T extends ZodRawShape">
|
||||
import type { ZodAny, ZodObject, ZodRawShape } from 'zod'
|
||||
import type { Config, ConfigItem, Shape } from './interface'
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
|
||||
import { FormItem } from '@/components/ui/form'
|
||||
import { FieldContextKey, useField } from 'vee-validate'
|
||||
import { computed, provide } from 'vue'
|
||||
import AutoFormField from './AutoFormField.vue'
|
||||
import AutoFormLabel from './AutoFormLabel.vue'
|
||||
import { beautifyObjectName, getBaseSchema, getBaseType, getDefaultValueInZodStack } from './utils'
|
||||
|
||||
const props = defineProps<{
|
||||
fieldName: string
|
||||
required?: boolean
|
||||
config?: Config<T>
|
||||
schema?: ZodObject<T>
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const shapes = computed(() => {
|
||||
// @ts-expect-error ignore {} not assignable to object
|
||||
const val: { [key in keyof T]: Shape } = {}
|
||||
|
||||
if (!props.schema)
|
||||
return
|
||||
const shape = getBaseSchema(props.schema)?.shape
|
||||
if (!shape)
|
||||
return
|
||||
Object.keys(shape).forEach((name) => {
|
||||
const item = shape[name] as ZodAny
|
||||
const baseItem = getBaseSchema(item) as ZodAny
|
||||
let options = (baseItem && 'values' in baseItem._def) ? baseItem._def.values as string[] : undefined
|
||||
if (!Array.isArray(options) && typeof options === 'object')
|
||||
options = Object.values(options)
|
||||
|
||||
val[name as keyof T] = {
|
||||
type: getBaseType(item),
|
||||
default: getDefaultValueInZodStack(item),
|
||||
options,
|
||||
required: !['ZodOptional', 'ZodNullable'].includes(item._def.typeName),
|
||||
schema: item,
|
||||
}
|
||||
})
|
||||
return val
|
||||
})
|
||||
|
||||
const fieldContext = useField(props.fieldName)
|
||||
// @ts-expect-error ignore missing `id`
|
||||
provide(FieldContextKey, fieldContext)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<slot v-bind="props">
|
||||
<Accordion type="single" as-child class="w-full" collapsible :disabled="disabled">
|
||||
<FormItem>
|
||||
<AccordionItem :value="fieldName" class="border-none">
|
||||
<AccordionTrigger>
|
||||
<AutoFormLabel class="text-base" :required="required">
|
||||
{{ schema?.description || beautifyObjectName(fieldName) }}
|
||||
</AutoFormLabel>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent class="p-1 space-y-5">
|
||||
<template v-for="(shape, key) in shapes" :key="key">
|
||||
<AutoFormField
|
||||
:config="config?.[key as keyof typeof config] as ConfigItem"
|
||||
:field-name="`${fieldName}.${key.toString()}`"
|
||||
:label="key.toString()"
|
||||
:shape="shape"
|
||||
/>
|
||||
</template>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</FormItem>
|
||||
</Accordion>
|
||||
</slot>
|
||||
</section>
|
||||
</template>
|
14
components/ui/auto-form/AutoFormLabel.vue
Normal file
14
components/ui/auto-form/AutoFormLabel.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { FormLabel } from '@/components/ui/form'
|
||||
|
||||
defineProps<{
|
||||
required?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormLabel>
|
||||
<slot />
|
||||
<span v-if="required" class="text-destructive"> *</span>
|
||||
</FormLabel>
|
||||
</template>
|
40
components/ui/auto-form/constant.ts
Normal file
40
components/ui/auto-form/constant.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import type { InputComponents } from './interface'
|
||||
import AutoFormFieldArray from './AutoFormFieldArray.vue'
|
||||
import AutoFormFieldBoolean from './AutoFormFieldBoolean.vue'
|
||||
import AutoFormFieldDate from './AutoFormFieldDate.vue'
|
||||
import AutoFormFieldEnum from './AutoFormFieldEnum.vue'
|
||||
import AutoFormFieldFile from './AutoFormFieldFile.vue'
|
||||
import AutoFormFieldInput from './AutoFormFieldInput.vue'
|
||||
import AutoFormFieldNumber from './AutoFormFieldNumber.vue'
|
||||
import AutoFormFieldObject from './AutoFormFieldObject.vue'
|
||||
|
||||
export const INPUT_COMPONENTS: InputComponents = {
|
||||
date: AutoFormFieldDate,
|
||||
select: AutoFormFieldEnum,
|
||||
radio: AutoFormFieldEnum,
|
||||
checkbox: AutoFormFieldBoolean,
|
||||
switch: AutoFormFieldBoolean,
|
||||
textarea: AutoFormFieldInput,
|
||||
number: AutoFormFieldNumber,
|
||||
string: AutoFormFieldInput,
|
||||
file: AutoFormFieldFile,
|
||||
array: AutoFormFieldArray,
|
||||
object: AutoFormFieldObject,
|
||||
}
|
||||
|
||||
/**
|
||||
* Define handlers for specific Zod types.
|
||||
* You can expand this object to support more types.
|
||||
*/
|
||||
export const DEFAULT_ZOD_HANDLERS: {
|
||||
[key: string]: keyof typeof INPUT_COMPONENTS
|
||||
} = {
|
||||
ZodString: 'string',
|
||||
ZodBoolean: 'checkbox',
|
||||
ZodDate: 'date',
|
||||
ZodEnum: 'select',
|
||||
ZodNativeEnum: 'select',
|
||||
ZodNumber: 'number',
|
||||
ZodArray: 'array',
|
||||
ZodObject: 'object',
|
||||
}
|
92
components/ui/auto-form/dependencies.ts
Normal file
92
components/ui/auto-form/dependencies.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import type { Ref } from 'vue'
|
||||
import type * as z from 'zod'
|
||||
import { createContext } from 'reka-ui'
|
||||
import { useFieldValue, useFormValues } from 'vee-validate'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { type Dependency, DependencyType, type EnumValues } from './interface'
|
||||
import { getFromPath, getIndexIfArray } from './utils'
|
||||
|
||||
export const [injectDependencies, provideDependencies] = createContext<Ref<Dependency<z.infer<z.ZodObject<any>>>[] | undefined>>('AutoFormDependencies')
|
||||
|
||||
export default function useDependencies(
|
||||
fieldName: string,
|
||||
) {
|
||||
const form = useFormValues()
|
||||
// parsed test[0].age => test.age
|
||||
const currentFieldName = fieldName.replace(/\[\d+\]/g, '')
|
||||
const currentFieldValue = useFieldValue<any>(fieldName)
|
||||
|
||||
if (!form)
|
||||
throw new Error('useDependencies should be used within <AutoForm>')
|
||||
|
||||
const dependencies = injectDependencies()
|
||||
const isDisabled = ref(false)
|
||||
const isHidden = ref(false)
|
||||
const isRequired = ref(false)
|
||||
const overrideOptions = ref<EnumValues | undefined>()
|
||||
|
||||
const currentFieldDependencies = computed(() => dependencies.value?.filter(
|
||||
dependency => dependency.targetField === currentFieldName,
|
||||
))
|
||||
|
||||
function getSourceValue(dep: Dependency<any>) {
|
||||
const source = dep.sourceField as string
|
||||
const index = getIndexIfArray(fieldName) ?? -1
|
||||
const [sourceLast, ...sourceInitial] = source.split('.').toReversed()
|
||||
const [_targetLast, ...targetInitial] = (dep.targetField as string).split('.').toReversed()
|
||||
|
||||
if (index >= 0 && sourceInitial.join(',') === targetInitial.join(',')) {
|
||||
const [_currentLast, ...currentInitial] = fieldName.split('.').toReversed()
|
||||
return getFromPath(form.value, currentInitial.join('.') + sourceLast);
|
||||
}
|
||||
|
||||
return getFromPath(form.value, source)
|
||||
}
|
||||
|
||||
const sourceFieldValues = computed(() => currentFieldDependencies.value?.map(dep => getSourceValue(dep)))
|
||||
|
||||
const resetConditionState = () => {
|
||||
isDisabled.value = false
|
||||
isHidden.value = false
|
||||
isRequired.value = false
|
||||
overrideOptions.value = undefined
|
||||
}
|
||||
|
||||
watch([sourceFieldValues, dependencies], () => {
|
||||
resetConditionState()
|
||||
currentFieldDependencies.value?.forEach((dep) => {
|
||||
const sourceValue = getSourceValue(dep)
|
||||
const conditionMet = dep.when(sourceValue, currentFieldValue.value)
|
||||
|
||||
switch (dep.type) {
|
||||
case DependencyType.DISABLES:
|
||||
if (conditionMet)
|
||||
isDisabled.value = true
|
||||
|
||||
break
|
||||
case DependencyType.REQUIRES:
|
||||
if (conditionMet)
|
||||
isRequired.value = true
|
||||
|
||||
break
|
||||
case DependencyType.HIDES:
|
||||
if (conditionMet)
|
||||
isHidden.value = true
|
||||
|
||||
break
|
||||
case DependencyType.SETS_OPTIONS:
|
||||
if (conditionMet)
|
||||
overrideOptions.value = dep.options
|
||||
|
||||
break
|
||||
}
|
||||
})
|
||||
}, { immediate: true, deep: true })
|
||||
|
||||
return {
|
||||
isDisabled,
|
||||
isHidden,
|
||||
isRequired,
|
||||
overrideOptions,
|
||||
}
|
||||
}
|
15
components/ui/auto-form/index.ts
Normal file
15
components/ui/auto-form/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export { default as AutoForm } from './AutoForm.vue'
|
||||
export { default as AutoFormField } from './AutoFormField.vue'
|
||||
|
||||
export { default as AutoFormFieldArray } from './AutoFormFieldArray.vue'
|
||||
export { default as AutoFormFieldBoolean } from './AutoFormFieldBoolean.vue'
|
||||
export { default as AutoFormFieldDate } from './AutoFormFieldDate.vue'
|
||||
|
||||
export { default as AutoFormFieldEnum } from './AutoFormFieldEnum.vue'
|
||||
export { default as AutoFormFieldFile } from './AutoFormFieldFile.vue'
|
||||
export { default as AutoFormFieldInput } from './AutoFormFieldInput.vue'
|
||||
export { default as AutoFormFieldNumber } from './AutoFormFieldNumber.vue'
|
||||
export { default as AutoFormFieldObject } from './AutoFormFieldObject.vue'
|
||||
export { default as AutoFormLabel } from './AutoFormLabel.vue'
|
||||
export type { Config, ConfigItem, FieldProps } from './interface'
|
||||
export { getBaseSchema, getBaseType, getObjectFormSchema } from './utils'
|
95
components/ui/auto-form/interface.ts
Normal file
95
components/ui/auto-form/interface.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import type { Component, InputHTMLAttributes } from 'vue'
|
||||
import type { z, ZodAny } from 'zod'
|
||||
import type { INPUT_COMPONENTS } from './constant'
|
||||
|
||||
export interface FieldProps {
|
||||
fieldName: string
|
||||
label?: string
|
||||
required?: boolean
|
||||
config?: ConfigItem
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export interface Shape {
|
||||
type: string
|
||||
default?: any
|
||||
required?: boolean
|
||||
options?: string[]
|
||||
schema?: ZodAny
|
||||
}
|
||||
|
||||
export interface InputComponents {
|
||||
date: Component
|
||||
select: Component
|
||||
radio: Component
|
||||
checkbox: Component
|
||||
switch: Component
|
||||
textarea: Component
|
||||
number: Component
|
||||
string: Component
|
||||
file: Component
|
||||
array: Component
|
||||
object: Component
|
||||
}
|
||||
|
||||
export interface ConfigItem {
|
||||
/** Value for the `FormLabel` */
|
||||
label?: string
|
||||
/** Value for the `FormDescription` */
|
||||
description?: string
|
||||
/** Pick which component to be rendered. */
|
||||
component?: keyof typeof INPUT_COMPONENTS | Component
|
||||
/** Hide `FormLabel`. */
|
||||
hideLabel?: boolean
|
||||
inputProps?: InputHTMLAttributes
|
||||
}
|
||||
|
||||
// Define a type to unwrap an array
|
||||
type UnwrapArray<T> = T extends (infer U)[] ? U : never
|
||||
|
||||
export type Config<SchemaType extends object> = {
|
||||
// If SchemaType.key is an object, create a nested Config, otherwise ConfigItem
|
||||
[Key in keyof SchemaType]?:
|
||||
SchemaType[Key] extends any[]
|
||||
? UnwrapArray<Config<SchemaType[Key]>>
|
||||
: SchemaType[Key] extends object
|
||||
? Config<SchemaType[Key]>
|
||||
: ConfigItem;
|
||||
}
|
||||
|
||||
export enum DependencyType {
|
||||
DISABLES,
|
||||
REQUIRES,
|
||||
HIDES,
|
||||
SETS_OPTIONS,
|
||||
}
|
||||
|
||||
interface BaseDependency<SchemaType extends z.infer<z.ZodObject<any, any>>> {
|
||||
sourceField: keyof SchemaType
|
||||
type: DependencyType
|
||||
targetField: keyof SchemaType
|
||||
when: (sourceFieldValue: any, targetFieldValue: any) => boolean
|
||||
}
|
||||
|
||||
export type ValueDependency<SchemaType extends z.infer<z.ZodObject<any, any>>> =
|
||||
BaseDependency<SchemaType> & {
|
||||
type:
|
||||
| DependencyType.DISABLES
|
||||
| DependencyType.REQUIRES
|
||||
| DependencyType.HIDES
|
||||
}
|
||||
|
||||
export type EnumValues = readonly [string, ...string[]]
|
||||
|
||||
export type OptionsDependency<
|
||||
SchemaType extends z.infer<z.ZodObject<any, any>>,
|
||||
> = BaseDependency<SchemaType> & {
|
||||
type: DependencyType.SETS_OPTIONS
|
||||
|
||||
// Partial array of values from sourceField that will trigger the dependency
|
||||
options: EnumValues
|
||||
}
|
||||
|
||||
export type Dependency<SchemaType extends z.infer<z.ZodObject<any, any>>> =
|
||||
| ValueDependency<SchemaType>
|
||||
| OptionsDependency<SchemaType>
|
188
components/ui/auto-form/utils.ts
Normal file
188
components/ui/auto-form/utils.ts
Normal file
@ -0,0 +1,188 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
// TODO: This should support recursive ZodEffects but TypeScript doesn't allow circular type definitions.
|
||||
export type ZodObjectOrWrapped =
|
||||
| z.ZodObject<any, any>
|
||||
| z.ZodEffects<z.ZodObject<any, any>>
|
||||
|
||||
/**
|
||||
* Beautify a camelCase string.
|
||||
* e.g. "myString" -> "My String"
|
||||
*/
|
||||
export function beautifyObjectName(string: string) {
|
||||
// Remove bracketed indices
|
||||
// if numbers only return the string
|
||||
let output = string.replace(/\[\d+\]/g, '').replace(/([A-Z])/g, ' $1')
|
||||
output = output.charAt(0).toUpperCase() + output.slice(1)
|
||||
return output
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse string and extract the index
|
||||
* @param string
|
||||
* @returns index or undefined
|
||||
*/
|
||||
export function getIndexIfArray(string: string) {
|
||||
const indexRegex = /\[(\d+)\]/
|
||||
// Match the index
|
||||
const match = string.match(indexRegex)
|
||||
// Extract the index (number)
|
||||
const index = match ? Number.parseInt(match[1]) : undefined
|
||||
return index
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the lowest level Zod type.
|
||||
* This will unpack optionals, refinements, etc.
|
||||
*/
|
||||
export function getBaseSchema<
|
||||
ChildType extends z.ZodAny | z.AnyZodObject = z.ZodAny,
|
||||
>(schema: ChildType | z.ZodEffects<ChildType>): ChildType | null {
|
||||
if (!schema)
|
||||
return null;
|
||||
if ('innerType' in schema._def)
|
||||
return getBaseSchema(schema._def.innerType as ChildType)
|
||||
|
||||
if ('schema' in schema._def)
|
||||
return getBaseSchema(schema._def.schema as ChildType)
|
||||
|
||||
return schema as ChildType
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type name of the lowest level Zod type.
|
||||
* This will unpack optionals, refinements, etc.
|
||||
*/
|
||||
export function getBaseType(schema: z.ZodAny) {
|
||||
const baseSchema = getBaseSchema(schema)
|
||||
return baseSchema ? baseSchema._def.typeName : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a "ZodDefault" in the Zod stack and return its value.
|
||||
*/
|
||||
export function getDefaultValueInZodStack(schema: z.ZodAny): any {
|
||||
const typedSchema = schema as unknown as z.ZodDefault<
|
||||
z.ZodNumber | z.ZodString
|
||||
>
|
||||
|
||||
if (typedSchema._def.typeName === 'ZodDefault')
|
||||
return typedSchema._def.defaultValue()
|
||||
|
||||
if ('innerType' in typedSchema._def) {
|
||||
return getDefaultValueInZodStack(
|
||||
typedSchema._def.innerType as unknown as z.ZodAny,
|
||||
)
|
||||
}
|
||||
if ('schema' in typedSchema._def) {
|
||||
return getDefaultValueInZodStack(
|
||||
(typedSchema._def as any).schema as z.ZodAny,
|
||||
)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function getObjectFormSchema(
|
||||
schema: ZodObjectOrWrapped,
|
||||
): z.ZodObject<any, any> {
|
||||
if (schema?._def.typeName === 'ZodEffects') {
|
||||
const typedSchema = schema as z.ZodEffects<z.ZodObject<any, any>>
|
||||
return getObjectFormSchema(typedSchema._def.schema)
|
||||
}
|
||||
return schema as z.ZodObject<any, any>
|
||||
}
|
||||
|
||||
function isIndex(value: unknown): value is number {
|
||||
return Number(value) >= 0;
|
||||
}
|
||||
/**
|
||||
* Constructs a path with dot paths for arrays to use brackets to be compatible with vee-validate path syntax
|
||||
*/
|
||||
export function normalizeFormPath(path: string): string {
|
||||
const pathArr = path.split('.')
|
||||
if (!pathArr.length)
|
||||
return '';
|
||||
|
||||
let fullPath = String(pathArr[0])
|
||||
for (let i = 1; i < pathArr.length; i++) {
|
||||
if (isIndex(pathArr[i])) {
|
||||
fullPath += `[${pathArr[i]}]`
|
||||
continue
|
||||
}
|
||||
|
||||
fullPath += `.${pathArr[i]}`
|
||||
}
|
||||
|
||||
return fullPath
|
||||
}
|
||||
|
||||
type NestedRecord = Record<string, unknown> | { [k: string]: NestedRecord }
|
||||
/**
|
||||
* Checks if the path opted out of nested fields using `[fieldName]` syntax
|
||||
*/
|
||||
export function isNotNestedPath(path: string) {
|
||||
return /^\[.+\]$/.test(path)
|
||||
}
|
||||
function isObject(obj: unknown): obj is Record<string, unknown> {
|
||||
return obj !== null && !!obj && typeof obj === 'object' && !Array.isArray(obj);
|
||||
}
|
||||
function isContainerValue(value: unknown): value is Record<string, unknown> {
|
||||
return isObject(value) || Array.isArray(value)
|
||||
}
|
||||
function cleanupNonNestedPath(path: string) {
|
||||
if (isNotNestedPath(path))
|
||||
return path.replace(/\[|\]/g, '');
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a nested property value from an object
|
||||
*/
|
||||
export function getFromPath<TValue = unknown>(object: NestedRecord | undefined, path: string): TValue | undefined
|
||||
export function getFromPath<TValue = unknown, TFallback = TValue>(
|
||||
object: NestedRecord | undefined,
|
||||
path: string,
|
||||
fallback?: TFallback,
|
||||
): TValue | TFallback
|
||||
export function getFromPath<TValue = unknown, TFallback = TValue>(
|
||||
object: NestedRecord | undefined,
|
||||
path: string,
|
||||
fallback?: TFallback,
|
||||
): TValue | TFallback | undefined {
|
||||
if (!object)
|
||||
return fallback
|
||||
|
||||
if (isNotNestedPath(path))
|
||||
return object[cleanupNonNestedPath(path)] as TValue | undefined
|
||||
|
||||
const resolvedValue = (path || '')
|
||||
.split(/\.|\[(\d+)\]/)
|
||||
.filter(Boolean)
|
||||
.reduce((acc, propKey) => {
|
||||
if (isContainerValue(acc) && propKey in acc)
|
||||
return acc[propKey]
|
||||
|
||||
return fallback
|
||||
}, object as unknown)
|
||||
|
||||
return resolvedValue as TValue | undefined
|
||||
}
|
||||
|
||||
type Booleanish = boolean | 'true' | 'false'
|
||||
|
||||
export function booleanishToBoolean(value: Booleanish) {
|
||||
switch (value) {
|
||||
case 'true':
|
||||
case true:
|
||||
return true;
|
||||
case 'false':
|
||||
case false:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function maybeBooleanishToBoolean(value?: Booleanish) {
|
||||
return value ? booleanishToBoolean(value) : undefined
|
||||
}
|
60
components/ui/calendar/Calendar.vue
Normal file
60
components/ui/calendar/Calendar.vue
Normal file
@ -0,0 +1,60 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@/lib/utils'
|
||||
import { CalendarRoot, type CalendarRootEmits, type CalendarRootProps, useForwardPropsEmits } from 'reka-ui'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading, CalendarNextButton, CalendarPrevButton } from '.'
|
||||
|
||||
const props = defineProps<CalendarRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const emits = defineEmits<CalendarRootEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarRoot
|
||||
v-slot="{ grid, weekDays }"
|
||||
:class="cn('p-3', props.class)"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<CalendarHeader>
|
||||
<CalendarPrevButton />
|
||||
<CalendarHeading />
|
||||
<CalendarNextButton />
|
||||
</CalendarHeader>
|
||||
|
||||
<div class="flex flex-col gap-y-4 mt-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
|
||||
<CalendarGrid v-for="month in grid" :key="month.value.toString()">
|
||||
<CalendarGridHead>
|
||||
<CalendarGridRow>
|
||||
<CalendarHeadCell
|
||||
v-for="day in weekDays" :key="day"
|
||||
>
|
||||
{{ day }}
|
||||
</CalendarHeadCell>
|
||||
</CalendarGridRow>
|
||||
</CalendarGridHead>
|
||||
<CalendarGridBody>
|
||||
<CalendarGridRow v-for="(weekDates, index) in month.rows" :key="`weekDate-${index}`" class="mt-2 w-full">
|
||||
<CalendarCell
|
||||
v-for="weekDate in weekDates"
|
||||
:key="weekDate.toString()"
|
||||
:date="weekDate"
|
||||
>
|
||||
<CalendarCellTrigger
|
||||
:day="weekDate"
|
||||
:month="month.value"
|
||||
/>
|
||||
</CalendarCell>
|
||||
</CalendarGridRow>
|
||||
</CalendarGridBody>
|
||||
</CalendarGrid>
|
||||
</div>
|
||||
</CalendarRoot>
|
||||
</template>
|
24
components/ui/calendar/CalendarCell.vue
Normal file
24
components/ui/calendar/CalendarCell.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@/lib/utils'
|
||||
import { CalendarCell, type CalendarCellProps, useForwardProps } from 'reka-ui'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<CalendarCellProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarCell
|
||||
:class="cn('relative h-9 w-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-view])]:bg-accent/50', props.class)"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot />
|
||||
</CalendarCell>
|
||||
</template>
|
38
components/ui/calendar/CalendarCellTrigger.vue
Normal file
38
components/ui/calendar/CalendarCellTrigger.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
import { CalendarCellTrigger, type CalendarCellTriggerProps, useForwardProps } from 'reka-ui'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<CalendarCellTriggerProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarCellTrigger
|
||||
:class="cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'h-9 w-9 p-0 font-normal',
|
||||
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
|
||||
// Selected
|
||||
'data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:opacity-100 data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground',
|
||||
// Disabled
|
||||
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
|
||||
// Unavailable
|
||||
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
|
||||
// Outside months
|
||||
'data-[outside-view]:text-muted-foreground data-[outside-view]:opacity-50 [&[data-outside-view][data-selected]]:bg-accent/50 [&[data-outside-view][data-selected]]:text-muted-foreground [&[data-outside-view][data-selected]]:opacity-30',
|
||||
props.class,
|
||||
)"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot />
|
||||
</CalendarCellTrigger>
|
||||
</template>
|
24
components/ui/calendar/CalendarGrid.vue
Normal file
24
components/ui/calendar/CalendarGrid.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@/lib/utils'
|
||||
import { CalendarGrid, type CalendarGridProps, useForwardProps } from 'reka-ui'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<CalendarGridProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarGrid
|
||||
:class="cn('w-full border-collapse space-y-1', props.class)"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot />
|
||||
</CalendarGrid>
|
||||
</template>
|
11
components/ui/calendar/CalendarGridBody.vue
Normal file
11
components/ui/calendar/CalendarGridBody.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { CalendarGridBody, type CalendarGridBodyProps } from 'reka-ui'
|
||||
|
||||
const props = defineProps<CalendarGridBodyProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarGridBody v-bind="props">
|
||||
<slot />
|
||||
</CalendarGridBody>
|
||||
</template>
|
11
components/ui/calendar/CalendarGridHead.vue
Normal file
11
components/ui/calendar/CalendarGridHead.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { CalendarGridHead, type CalendarGridHeadProps } from 'reka-ui'
|
||||
|
||||
const props = defineProps<CalendarGridHeadProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarGridHead v-bind="props">
|
||||
<slot />
|
||||
</CalendarGridHead>
|
||||
</template>
|
21
components/ui/calendar/CalendarGridRow.vue
Normal file
21
components/ui/calendar/CalendarGridRow.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@/lib/utils'
|
||||
import { CalendarGridRow, type CalendarGridRowProps, useForwardProps } from 'reka-ui'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<CalendarGridRowProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarGridRow :class="cn('flex', props.class)" v-bind="forwardedProps">
|
||||
<slot />
|
||||
</CalendarGridRow>
|
||||
</template>
|
21
components/ui/calendar/CalendarHeadCell.vue
Normal file
21
components/ui/calendar/CalendarHeadCell.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@/lib/utils'
|
||||
import { CalendarHeadCell, type CalendarHeadCellProps, useForwardProps } from 'reka-ui'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<CalendarHeadCellProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarHeadCell :class="cn('w-9 rounded-md text-[0.8rem] font-normal text-muted-foreground', props.class)" v-bind="forwardedProps">
|
||||
<slot />
|
||||
</CalendarHeadCell>
|
||||
</template>
|
21
components/ui/calendar/CalendarHeader.vue
Normal file
21
components/ui/calendar/CalendarHeader.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@/lib/utils'
|
||||
import { CalendarHeader, type CalendarHeaderProps, useForwardProps } from 'reka-ui'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<CalendarHeaderProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarHeader :class="cn('relative flex w-full items-center justify-between pt-1', props.class)" v-bind="forwardedProps">
|
||||
<slot />
|
||||
</CalendarHeader>
|
||||
</template>
|
31
components/ui/calendar/CalendarHeading.vue
Normal file
31
components/ui/calendar/CalendarHeading.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@/lib/utils'
|
||||
import { CalendarHeading, type CalendarHeadingProps, useForwardProps } from 'reka-ui'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<CalendarHeadingProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
defineSlots<{
|
||||
default: (props: { headingValue: string }) => any
|
||||
}>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarHeading
|
||||
v-slot="{ headingValue }"
|
||||
:class="cn('text-sm font-medium', props.class)"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot :heading-value>
|
||||
{{ headingValue }}
|
||||
</slot>
|
||||
</CalendarHeading>
|
||||
</template>
|
32
components/ui/calendar/CalendarNextButton.vue
Normal file
32
components/ui/calendar/CalendarNextButton.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
import { ChevronRight } from 'lucide-vue-next'
|
||||
import { CalendarNext, type CalendarNextProps, useForwardProps } from 'reka-ui'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<CalendarNextProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarNext
|
||||
:class="cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||
props.class,
|
||||
)"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot>
|
||||
<ChevronRight class="h-4 w-4" />
|
||||
</slot>
|
||||
</CalendarNext>
|
||||
</template>
|
32
components/ui/calendar/CalendarPrevButton.vue
Normal file
32
components/ui/calendar/CalendarPrevButton.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
import { ChevronLeft } from 'lucide-vue-next'
|
||||
import { CalendarPrev, type CalendarPrevProps, useForwardProps } from 'reka-ui'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<CalendarPrevProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarPrev
|
||||
:class="cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||
props.class,
|
||||
)"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot>
|
||||
<ChevronLeft class="h-4 w-4" />
|
||||
</slot>
|
||||
</CalendarPrev>
|
||||
</template>
|
12
components/ui/calendar/index.ts
Normal file
12
components/ui/calendar/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export { default as Calendar } from './Calendar.vue'
|
||||
export { default as CalendarCell } from './CalendarCell.vue'
|
||||
export { default as CalendarCellTrigger } from './CalendarCellTrigger.vue'
|
||||
export { default as CalendarGrid } from './CalendarGrid.vue'
|
||||
export { default as CalendarGridBody } from './CalendarGridBody.vue'
|
||||
export { default as CalendarGridHead } from './CalendarGridHead.vue'
|
||||
export { default as CalendarGridRow } from './CalendarGridRow.vue'
|
||||
export { default as CalendarHeadCell } from './CalendarHeadCell.vue'
|
||||
export { default as CalendarHeader } from './CalendarHeader.vue'
|
||||
export { default as CalendarHeading } from './CalendarHeading.vue'
|
||||
export { default as CalendarNextButton } from './CalendarNextButton.vue'
|
||||
export { default as CalendarPrevButton } from './CalendarPrevButton.vue'
|
33
components/ui/checkbox/Checkbox.vue
Normal file
33
components/ui/checkbox/Checkbox.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import type { CheckboxRootEmits, CheckboxRootProps } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Check } from 'lucide-vue-next'
|
||||
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<CheckboxRootEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CheckboxRoot
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn('peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
|
||||
props.class)"
|
||||
>
|
||||
<CheckboxIndicator class="flex h-full w-full items-center justify-center text-current">
|
||||
<slot>
|
||||
<Check class="h-4 w-4" />
|
||||
</slot>
|
||||
</CheckboxIndicator>
|
||||
</CheckboxRoot>
|
||||
</template>
|
1
components/ui/checkbox/index.ts
Normal file
1
components/ui/checkbox/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as Checkbox } from './Checkbox.vue'
|
25
components/ui/radio-group/RadioGroup.vue
Normal file
25
components/ui/radio-group/RadioGroup.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import { RadioGroupRoot, type RadioGroupRootEmits, type RadioGroupRootProps, useForwardPropsEmits } from 'reka-ui'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<RadioGroupRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<RadioGroupRootEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RadioGroupRoot
|
||||
:class="cn('grid gap-2', props.class)"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot />
|
||||
</RadioGroupRoot>
|
||||
</template>
|
41
components/ui/radio-group/RadioGroupItem.vue
Normal file
41
components/ui/radio-group/RadioGroupItem.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import type { RadioGroupItemProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Circle } from 'lucide-vue-next'
|
||||
import {
|
||||
RadioGroupIndicator,
|
||||
RadioGroupItem,
|
||||
|
||||
useForwardProps,
|
||||
} from 'reka-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<RadioGroupItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RadioGroupItem
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'peer aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<RadioGroupIndicator
|
||||
class="flex items-center justify-center"
|
||||
>
|
||||
<Circle class="h-2.5 w-2.5 fill-current text-current" />
|
||||
</RadioGroupIndicator>
|
||||
</RadioGroupItem>
|
||||
</template>
|
2
components/ui/radio-group/index.ts
Normal file
2
components/ui/radio-group/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as RadioGroup } from './RadioGroup.vue'
|
||||
export { default as RadioGroupItem } from './RadioGroupItem.vue'
|
39
components/ui/switch/Switch.vue
Normal file
39
components/ui/switch/Switch.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
SwitchRoot,
|
||||
type SwitchRootEmits,
|
||||
type SwitchRootProps,
|
||||
SwitchThumb,
|
||||
useForwardPropsEmits,
|
||||
} from 'reka-ui'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<SwitchRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const emits = defineEmits<SwitchRootEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SwitchRoot
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<SwitchThumb
|
||||
:class="cn('pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5')"
|
||||
>
|
||||
<slot name="thumb" />
|
||||
</SwitchThumb>
|
||||
</SwitchRoot>
|
||||
</template>
|
1
components/ui/switch/index.ts
Normal file
1
components/ui/switch/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as Switch } from './Switch.vue'
|
@ -14,6 +14,7 @@ export default defineNuxtConfig({
|
||||
'dayjs-nuxt',
|
||||
'@formkit/auto-animate',
|
||||
'@vueuse/nuxt',
|
||||
'nuxt-lodash',
|
||||
],
|
||||
ssr: false,
|
||||
devtools: { enabled: true },
|
||||
@ -56,4 +57,4 @@ export default defineNuxtConfig({
|
||||
*/
|
||||
componentDir: './components/ui',
|
||||
},
|
||||
})
|
||||
})
|
@ -28,7 +28,9 @@
|
||||
"dotenv": "^16.5.0",
|
||||
"dplayer": "^1.27.1",
|
||||
"eslint": "^9.0.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"lucide-vue-next": "^0.484.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"nuxt": "^3.16.1",
|
||||
"reka-ui": "^2.1.1",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
@ -54,12 +56,14 @@
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@testing-library/vue": "^8.1.0",
|
||||
"@types/dplayer": "^1.25.5",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vueuse/nuxt": "^13.0.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs-nuxt": "^2.1.11",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"happy-dom": "^17.4.4",
|
||||
"nuxt-lodash": "^2.5.3",
|
||||
"pinia-plugin-persistedstate": "^4.2.0",
|
||||
"playwright-core": "^1.52.0",
|
||||
"prettier": "^3.5.3",
|
||||
|
@ -1,6 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { nav } from './config'
|
||||
import { FnTeachLessonPlan } from '#components'
|
||||
import {
|
||||
FnTeachLessonPlan,
|
||||
FnTeachCaseGen,
|
||||
FnTeachStdDesign,
|
||||
FnTeachKnowledgeDiagram,
|
||||
} from '#components'
|
||||
import type { NavTertiaryItem } from '~/components/nav/Tertiary.vue'
|
||||
|
||||
definePageMeta({
|
||||
@ -18,9 +23,9 @@ const { setBreadcrumbs } = useBreadcrumbs()
|
||||
|
||||
const tertiaryNavs: NavTertiaryItem[] = [
|
||||
{ label: '教案设计', component: FnTeachLessonPlan },
|
||||
{ label: '案例设计' },
|
||||
{ label: '课程标准' },
|
||||
{ label: '知识图谱' },
|
||||
{ label: '案例设计', component: FnTeachCaseGen },
|
||||
{ label: '课程标准', component: FnTeachStdDesign },
|
||||
{ label: '知识图谱', component: FnTeachKnowledgeDiagram },
|
||||
{ label: '课程章节' },
|
||||
{ label: '教研计划' },
|
||||
]
|
||||
@ -74,10 +79,13 @@ onMounted(() => {
|
||||
</div>
|
||||
<div class="flex-1 h-full p-6">
|
||||
<Suspense>
|
||||
<component
|
||||
:is="tertiaryNavs[currentNav].component"
|
||||
v-if="tertiaryNavs[currentNav].component"
|
||||
/>
|
||||
<KeepAlive v-if="tertiaryNavs[currentNav].component">
|
||||
<component
|
||||
:is="tertiaryNavs[currentNav].component"
|
||||
v-bind="tertiaryNavs[currentNav].props"
|
||||
class="w-full h-full"
|
||||
/>
|
||||
</KeepAlive>
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col items-center justify-center w-full h-full gap-2"
|
||||
@ -86,9 +94,7 @@ onMounted(() => {
|
||||
name="tabler:mood-sad"
|
||||
class="text-6xl text-muted-foreground"
|
||||
/>
|
||||
<p class="text text-muted-foreground">
|
||||
该功能暂不可用
|
||||
</p>
|
||||
<p class="text text-muted-foreground">该功能暂不可用</p>
|
||||
</div>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
248
pnpm-lock.yaml
generated
248
pnpm-lock.yaml
generated
@ -10,10 +10,10 @@ importers:
|
||||
dependencies:
|
||||
'@nuxt/fonts':
|
||||
specifier: 0.11.0
|
||||
version: 0.11.0(db0@0.3.1)(ioredis@5.6.0)(magicast@0.3.5)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))
|
||||
version: 0.11.0(db0@0.3.1)(ioredis@5.6.0)(magicast@0.3.5)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))
|
||||
'@nuxt/icon':
|
||||
specifier: 1.11.0
|
||||
version: 1.11.0(magicast@0.3.5)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
|
||||
version: 1.11.0(magicast@0.3.5)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.2))
|
||||
'@nuxt/image':
|
||||
specifier: 1.10.0
|
||||
version: 1.10.0(db0@0.3.1)(ioredis@5.6.0)(magicast@0.3.5)
|
||||
@ -53,12 +53,18 @@ importers:
|
||||
eslint:
|
||||
specifier: ^9.0.0
|
||||
version: 9.23.0(jiti@2.4.2)
|
||||
highlight.js:
|
||||
specifier: ^11.11.1
|
||||
version: 11.11.1
|
||||
lucide-vue-next:
|
||||
specifier: ^0.484.0
|
||||
version: 0.484.0(vue@3.5.13(typescript@5.8.2))
|
||||
markdown-it:
|
||||
specifier: ^14.1.0
|
||||
version: 14.1.0
|
||||
nuxt:
|
||||
specifier: ^3.16.1
|
||||
version: 3.16.1(@parcel/watcher@2.5.1)(db0@0.3.1)(eslint@9.23.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.37.0)(terser@5.39.0)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0)
|
||||
version: 3.16.1(@parcel/watcher@2.5.1)(db0@0.3.1)(eslint@9.23.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.37.0)(terser@5.39.0)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(yaml@2.7.1)
|
||||
reka-ui:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))
|
||||
@ -101,10 +107,10 @@ importers:
|
||||
version: 1.2.17
|
||||
'@nuxt/eslint':
|
||||
specifier: 1.3.0
|
||||
version: 1.3.0(@vue/compiler-sfc@3.5.13)(eslint@9.23.0(jiti@2.4.2))(magicast@0.3.5)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))
|
||||
version: 1.3.0(@vue/compiler-sfc@3.5.13)(eslint@9.23.0(jiti@2.4.2))(magicast@0.3.5)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))
|
||||
'@nuxt/test-utils':
|
||||
specifier: 3.17.2
|
||||
version: 3.17.2(@testing-library/vue@8.1.0(@vue/compiler-sfc@3.5.13)(vue@3.5.13(typescript@5.8.2)))(@vue/test-utils@2.4.6)(happy-dom@17.4.4)(jiti@2.4.2)(magicast@0.3.5)(playwright-core@1.52.0)(terser@5.39.0)(typescript@5.8.2)(vitest@3.1.1(happy-dom@17.4.4)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0)
|
||||
version: 3.17.2(@testing-library/vue@8.1.0(@vue/compiler-sfc@3.5.13)(vue@3.5.13(typescript@5.8.2)))(@vue/test-utils@2.4.6)(happy-dom@17.4.4)(jiti@2.4.2)(magicast@0.3.5)(playwright-core@1.52.0)(terser@5.39.0)(typescript@5.8.2)(vitest@3.1.1(happy-dom@17.4.4)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(yaml@2.7.1)
|
||||
'@nuxtjs/color-mode':
|
||||
specifier: ^3.5.2
|
||||
version: 3.5.2(magicast@0.3.5)
|
||||
@ -123,12 +129,15 @@ importers:
|
||||
'@types/dplayer':
|
||||
specifier: ^1.25.5
|
||||
version: 1.25.5
|
||||
'@types/markdown-it':
|
||||
specifier: ^14.1.2
|
||||
version: 14.1.2
|
||||
'@vue/test-utils':
|
||||
specifier: ^2.4.6
|
||||
version: 2.4.6
|
||||
'@vueuse/nuxt':
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0(magicast@0.3.5)(nuxt@3.16.1(@parcel/watcher@2.5.1)(db0@0.3.1)(eslint@9.23.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.37.0)(terser@5.39.0)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
|
||||
version: 13.0.0(magicast@0.3.5)(nuxt@3.16.1(@parcel/watcher@2.5.1)(db0@0.3.1)(eslint@9.23.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.37.0)(terser@5.39.0)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(yaml@2.7.1))(vue@3.5.13(typescript@5.8.2))
|
||||
dayjs:
|
||||
specifier: ^1.11.13
|
||||
version: 1.11.13
|
||||
@ -141,6 +150,9 @@ importers:
|
||||
happy-dom:
|
||||
specifier: ^17.4.4
|
||||
version: 17.4.4
|
||||
nuxt-lodash:
|
||||
specifier: ^2.5.3
|
||||
version: 2.5.3(magicast@0.3.5)
|
||||
pinia-plugin-persistedstate:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0(@pinia/nuxt@0.10.1(magicast@0.3.5)(pinia@3.0.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))))(magicast@0.3.5)(pinia@3.0.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2)))
|
||||
@ -158,7 +170,7 @@ importers:
|
||||
version: 5.8.2
|
||||
vitest:
|
||||
specifier: ^3.1.1
|
||||
version: 3.1.1(happy-dom@17.4.4)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)
|
||||
version: 3.1.1(happy-dom@17.4.4)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)
|
||||
|
||||
packages:
|
||||
|
||||
@ -1277,6 +1289,21 @@ packages:
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
'@types/linkify-it@5.0.0':
|
||||
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
||||
|
||||
'@types/lodash-es@4.17.12':
|
||||
resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
|
||||
|
||||
'@types/lodash@4.17.16':
|
||||
resolution: {integrity: sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==}
|
||||
|
||||
'@types/markdown-it@14.1.2':
|
||||
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
|
||||
|
||||
'@types/mdurl@2.0.0':
|
||||
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
|
||||
|
||||
'@types/normalize-package-data@2.4.4':
|
||||
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
|
||||
|
||||
@ -2869,6 +2896,10 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
highlight.js@11.11.1:
|
||||
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
hookable@5.5.3:
|
||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||
|
||||
@ -3262,6 +3293,9 @@ packages:
|
||||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
linkify-it@5.0.0:
|
||||
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
||||
|
||||
listhen@1.9.0:
|
||||
resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==}
|
||||
hasBin: true
|
||||
@ -3282,6 +3316,9 @@ packages:
|
||||
resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
lodash-es@4.17.21:
|
||||
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
|
||||
|
||||
lodash.castarray@4.4.0:
|
||||
resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
|
||||
|
||||
@ -3337,6 +3374,10 @@ packages:
|
||||
magicast@0.3.5:
|
||||
resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
|
||||
|
||||
markdown-it@14.1.0:
|
||||
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
|
||||
hasBin: true
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -3350,6 +3391,9 @@ packages:
|
||||
mdn-data@2.12.2:
|
||||
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
|
||||
|
||||
mdurl@2.0.0:
|
||||
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
|
||||
|
||||
media-typer@0.3.0:
|
||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@ -3566,6 +3610,9 @@ packages:
|
||||
nth-check@2.1.1:
|
||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||
|
||||
nuxt-lodash@2.5.3:
|
||||
resolution: {integrity: sha512-LCJ40tGQTHkfk8rQoGqtb0mFgRVt8U+pc3S352DsI39yF3olqgxlsm28wdJMIcWHuxLgf5X+DwxmZhrAHXNS5g==}
|
||||
|
||||
nuxt@3.16.1:
|
||||
resolution: {integrity: sha512-V0odAW9Yo8s58yGnSy0RuX+rQwz0wtQp3eOgMTsh1YDDZdIIYZmAlZaLypNeieO/mbmvOOUcnuRyIGIRrF4+5A==}
|
||||
engines: {node: ^18.12.0 || ^20.9.0 || >=22.0.0}
|
||||
@ -4084,6 +4131,10 @@ packages:
|
||||
pump@3.0.2:
|
||||
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
||||
|
||||
punycode.js@2.3.1:
|
||||
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
punycode@2.3.1:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
@ -4668,6 +4719,9 @@ packages:
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
uc.micro@2.1.0:
|
||||
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
|
||||
|
||||
ufo@1.5.4:
|
||||
resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
|
||||
|
||||
@ -5118,6 +5172,11 @@ packages:
|
||||
engines: {node: '>= 14'}
|
||||
hasBin: true
|
||||
|
||||
yaml@2.7.1:
|
||||
resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==}
|
||||
engines: {node: '>= 14'}
|
||||
hasBin: true
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
@ -5782,12 +5841,12 @@ snapshots:
|
||||
|
||||
'@nuxt/devalue@2.0.2': {}
|
||||
|
||||
'@nuxt/devtools-kit@2.3.2(magicast@0.3.5)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))':
|
||||
'@nuxt/devtools-kit@2.3.2(magicast@0.3.5)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))':
|
||||
dependencies:
|
||||
'@nuxt/kit': 3.16.1(magicast@0.3.5)
|
||||
'@nuxt/schema': 3.16.1
|
||||
execa: 8.0.1
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
@ -5802,12 +5861,12 @@ snapshots:
|
||||
prompts: 2.4.2
|
||||
semver: 7.7.1
|
||||
|
||||
'@nuxt/devtools@2.3.2(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))':
|
||||
'@nuxt/devtools@2.3.2(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.2))':
|
||||
dependencies:
|
||||
'@nuxt/devtools-kit': 2.3.2(magicast@0.3.5)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))
|
||||
'@nuxt/devtools-kit': 2.3.2(magicast@0.3.5)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))
|
||||
'@nuxt/devtools-wizard': 2.3.2
|
||||
'@nuxt/kit': 3.16.1(magicast@0.3.5)
|
||||
'@vue/devtools-core': 7.7.2(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue/devtools-core': 7.7.2(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue/devtools-kit': 7.7.2
|
||||
birpc: 2.3.0
|
||||
consola: 3.4.2
|
||||
@ -5832,9 +5891,9 @@ snapshots:
|
||||
sirv: 3.0.1
|
||||
structured-clone-es: 1.0.0
|
||||
tinyglobby: 0.2.12
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)
|
||||
vite-plugin-inspect: 11.0.0(@nuxt/kit@3.16.1(magicast@0.3.5))(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))
|
||||
vite-plugin-vue-tracer: 0.1.3(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)
|
||||
vite-plugin-inspect: 11.0.0(@nuxt/kit@3.16.1(magicast@0.3.5))(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))
|
||||
vite-plugin-vue-tracer: 0.1.3(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.2))
|
||||
which: 5.0.0
|
||||
ws: 8.18.1
|
||||
transitivePeerDependencies:
|
||||
@ -5880,10 +5939,10 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@nuxt/eslint@1.3.0(@vue/compiler-sfc@3.5.13)(eslint@9.23.0(jiti@2.4.2))(magicast@0.3.5)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))':
|
||||
'@nuxt/eslint@1.3.0(@vue/compiler-sfc@3.5.13)(eslint@9.23.0(jiti@2.4.2))(magicast@0.3.5)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))':
|
||||
dependencies:
|
||||
'@eslint/config-inspector': 1.0.2(eslint@9.23.0(jiti@2.4.2))
|
||||
'@nuxt/devtools-kit': 2.3.2(magicast@0.3.5)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))
|
||||
'@nuxt/devtools-kit': 2.3.2(magicast@0.3.5)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))
|
||||
'@nuxt/eslint-config': 1.3.0(@vue/compiler-sfc@3.5.13)(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)
|
||||
'@nuxt/eslint-plugin': 1.3.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)
|
||||
'@nuxt/kit': 3.16.1(magicast@0.3.5)
|
||||
@ -5906,9 +5965,9 @@ snapshots:
|
||||
- utf-8-validate
|
||||
- vite
|
||||
|
||||
'@nuxt/fonts@0.11.0(db0@0.3.1)(ioredis@5.6.0)(magicast@0.3.5)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))':
|
||||
'@nuxt/fonts@0.11.0(db0@0.3.1)(ioredis@5.6.0)(magicast@0.3.5)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))':
|
||||
dependencies:
|
||||
'@nuxt/devtools-kit': 2.3.2(magicast@0.3.5)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))
|
||||
'@nuxt/devtools-kit': 2.3.2(magicast@0.3.5)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))
|
||||
'@nuxt/kit': 3.16.1(magicast@0.3.5)
|
||||
consola: 3.4.2
|
||||
css-tree: 3.1.0
|
||||
@ -5951,13 +6010,13 @@ snapshots:
|
||||
- uploadthing
|
||||
- vite
|
||||
|
||||
'@nuxt/icon@1.11.0(magicast@0.3.5)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))':
|
||||
'@nuxt/icon@1.11.0(magicast@0.3.5)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.2))':
|
||||
dependencies:
|
||||
'@iconify/collections': 1.0.532
|
||||
'@iconify/types': 2.0.0
|
||||
'@iconify/utils': 2.3.0
|
||||
'@iconify/vue': 4.3.0(vue@3.5.13(typescript@5.8.2))
|
||||
'@nuxt/devtools-kit': 2.3.2(magicast@0.3.5)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))
|
||||
'@nuxt/devtools-kit': 2.3.2(magicast@0.3.5)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))
|
||||
'@nuxt/kit': 3.16.1(magicast@0.3.5)
|
||||
consola: 3.4.2
|
||||
local-pkg: 1.1.1
|
||||
@ -6060,7 +6119,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
'@nuxt/test-utils@3.17.2(@testing-library/vue@8.1.0(@vue/compiler-sfc@3.5.13)(vue@3.5.13(typescript@5.8.2)))(@vue/test-utils@2.4.6)(happy-dom@17.4.4)(jiti@2.4.2)(magicast@0.3.5)(playwright-core@1.52.0)(terser@5.39.0)(typescript@5.8.2)(vitest@3.1.1(happy-dom@17.4.4)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0)':
|
||||
'@nuxt/test-utils@3.17.2(@testing-library/vue@8.1.0(@vue/compiler-sfc@3.5.13)(vue@3.5.13(typescript@5.8.2)))(@vue/test-utils@2.4.6)(happy-dom@17.4.4)(jiti@2.4.2)(magicast@0.3.5)(playwright-core@1.52.0)(terser@5.39.0)(typescript@5.8.2)(vitest@3.1.1(happy-dom@17.4.4)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(yaml@2.7.1)':
|
||||
dependencies:
|
||||
'@nuxt/kit': 3.16.1(magicast@0.3.5)
|
||||
'@nuxt/schema': 3.16.1
|
||||
@ -6085,15 +6144,15 @@ snapshots:
|
||||
tinyexec: 0.3.2
|
||||
ufo: 1.5.4
|
||||
unplugin: 2.2.2
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)
|
||||
vitest-environment-nuxt: 1.0.1(@testing-library/vue@8.1.0(@vue/compiler-sfc@3.5.13)(vue@3.5.13(typescript@5.8.2)))(@vue/test-utils@2.4.6)(happy-dom@17.4.4)(jiti@2.4.2)(magicast@0.3.5)(playwright-core@1.52.0)(terser@5.39.0)(typescript@5.8.2)(vitest@3.1.1(happy-dom@17.4.4)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0)
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)
|
||||
vitest-environment-nuxt: 1.0.1(@testing-library/vue@8.1.0(@vue/compiler-sfc@3.5.13)(vue@3.5.13(typescript@5.8.2)))(@vue/test-utils@2.4.6)(happy-dom@17.4.4)(jiti@2.4.2)(magicast@0.3.5)(playwright-core@1.52.0)(terser@5.39.0)(typescript@5.8.2)(vitest@3.1.1(happy-dom@17.4.4)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(yaml@2.7.1)
|
||||
vue: 3.5.13(typescript@5.8.2)
|
||||
optionalDependencies:
|
||||
'@testing-library/vue': 8.1.0(@vue/compiler-sfc@3.5.13)(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue/test-utils': 2.4.6
|
||||
happy-dom: 17.4.4
|
||||
playwright-core: 1.52.0
|
||||
vitest: 3.1.1(happy-dom@17.4.4)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)
|
||||
vitest: 3.1.1(happy-dom@17.4.4)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
@ -6109,12 +6168,12 @@ snapshots:
|
||||
- typescript
|
||||
- yaml
|
||||
|
||||
'@nuxt/vite-builder@3.16.1(eslint@9.23.0(jiti@2.4.2))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.37.0)(terser@5.39.0)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))(yaml@2.7.0)':
|
||||
'@nuxt/vite-builder@3.16.1(eslint@9.23.0(jiti@2.4.2))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.37.0)(terser@5.39.0)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))(yaml@2.7.1)':
|
||||
dependencies:
|
||||
'@nuxt/kit': 3.16.1(magicast@0.3.5)
|
||||
'@rollup/plugin-replace': 6.0.2(rollup@4.37.0)
|
||||
'@vitejs/plugin-vue': 5.2.3(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
|
||||
'@vitejs/plugin-vue-jsx': 4.1.2(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
|
||||
'@vitejs/plugin-vue': 5.2.3(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.2))
|
||||
'@vitejs/plugin-vue-jsx': 4.1.2(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.2))
|
||||
autoprefixer: 10.4.21(postcss@8.5.3)
|
||||
consola: 3.4.2
|
||||
cssnano: 7.0.6(postcss@8.5.3)
|
||||
@ -6140,9 +6199,9 @@ snapshots:
|
||||
ufo: 1.5.4
|
||||
unenv: 2.0.0-rc.15
|
||||
unplugin: 2.2.2
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)
|
||||
vite-node: 3.0.9(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)
|
||||
vite-plugin-checker: 0.9.1(eslint@9.23.0(jiti@2.4.2))(optionator@0.9.4)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)
|
||||
vite-node: 3.0.9(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)
|
||||
vite-plugin-checker: 0.9.1(eslint@9.23.0(jiti@2.4.2))(optionator@0.9.4)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))
|
||||
vue: 3.5.13(typescript@5.8.2)
|
||||
vue-bundle-renderer: 2.1.1
|
||||
transitivePeerDependencies:
|
||||
@ -6548,6 +6607,21 @@ snapshots:
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/linkify-it@5.0.0': {}
|
||||
|
||||
'@types/lodash-es@4.17.12':
|
||||
dependencies:
|
||||
'@types/lodash': 4.17.16
|
||||
|
||||
'@types/lodash@4.17.16': {}
|
||||
|
||||
'@types/markdown-it@14.1.2':
|
||||
dependencies:
|
||||
'@types/linkify-it': 5.0.0
|
||||
'@types/mdurl': 2.0.0
|
||||
|
||||
'@types/mdurl@2.0.0': {}
|
||||
|
||||
'@types/normalize-package-data@2.4.4': {}
|
||||
|
||||
'@types/parse-path@7.0.3': {}
|
||||
@ -6753,19 +6827,19 @@ snapshots:
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
'@vitejs/plugin-vue-jsx@4.1.2(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))':
|
||||
'@vitejs/plugin-vue-jsx@4.1.2(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.2))':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.10
|
||||
'@babel/plugin-transform-typescript': 7.27.0(@babel/core@7.26.10)
|
||||
'@vue/babel-plugin-jsx': 1.4.0(@babel/core@7.26.10)
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)
|
||||
vue: 3.5.13(typescript@5.8.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitejs/plugin-vue@5.2.3(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))':
|
||||
'@vitejs/plugin-vue@5.2.3(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.2))':
|
||||
dependencies:
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)
|
||||
vue: 3.5.13(typescript@5.8.2)
|
||||
|
||||
'@vitest/expect@3.1.1':
|
||||
@ -6775,13 +6849,13 @@ snapshots:
|
||||
chai: 5.2.0
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/mocker@3.1.1(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))':
|
||||
'@vitest/mocker@3.1.1(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.1.1
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.17
|
||||
optionalDependencies:
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)
|
||||
|
||||
'@vitest/pretty-format@3.1.1':
|
||||
dependencies:
|
||||
@ -6904,14 +6978,14 @@ snapshots:
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 7.7.2
|
||||
|
||||
'@vue/devtools-core@7.7.2(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))':
|
||||
'@vue/devtools-core@7.7.2(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.2))':
|
||||
dependencies:
|
||||
'@vue/devtools-kit': 7.7.2
|
||||
'@vue/devtools-shared': 7.7.2
|
||||
mitt: 3.0.1
|
||||
nanoid: 5.1.5
|
||||
pathe: 2.0.3
|
||||
vite-hot-client: 0.2.4(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))
|
||||
vite-hot-client: 0.2.4(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))
|
||||
vue: 3.5.13(typescript@5.8.2)
|
||||
transitivePeerDependencies:
|
||||
- vite
|
||||
@ -6979,13 +7053,13 @@ snapshots:
|
||||
|
||||
'@vueuse/metadata@13.0.0': {}
|
||||
|
||||
'@vueuse/nuxt@13.0.0(magicast@0.3.5)(nuxt@3.16.1(@parcel/watcher@2.5.1)(db0@0.3.1)(eslint@9.23.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.37.0)(terser@5.39.0)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))':
|
||||
'@vueuse/nuxt@13.0.0(magicast@0.3.5)(nuxt@3.16.1(@parcel/watcher@2.5.1)(db0@0.3.1)(eslint@9.23.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.37.0)(terser@5.39.0)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(yaml@2.7.1))(vue@3.5.13(typescript@5.8.2))':
|
||||
dependencies:
|
||||
'@nuxt/kit': 3.16.1(magicast@0.3.5)
|
||||
'@vueuse/core': 13.0.0(vue@3.5.13(typescript@5.8.2))
|
||||
'@vueuse/metadata': 13.0.0
|
||||
local-pkg: 1.1.1
|
||||
nuxt: 3.16.1(@parcel/watcher@2.5.1)(db0@0.3.1)(eslint@9.23.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.37.0)(terser@5.39.0)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0)
|
||||
nuxt: 3.16.1(@parcel/watcher@2.5.1)(db0@0.3.1)(eslint@9.23.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.37.0)(terser@5.39.0)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(yaml@2.7.1)
|
||||
vue: 3.5.13(typescript@5.8.2)
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
@ -8322,6 +8396,8 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
highlight.js@11.11.1: {}
|
||||
|
||||
hookable@5.5.3: {}
|
||||
|
||||
hosted-git-info@7.0.2:
|
||||
@ -8760,6 +8836,10 @@ snapshots:
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
linkify-it@5.0.0:
|
||||
dependencies:
|
||||
uc.micro: 2.1.0
|
||||
|
||||
listhen@1.9.0:
|
||||
dependencies:
|
||||
'@parcel/watcher': 2.5.1
|
||||
@ -8797,6 +8877,8 @@ snapshots:
|
||||
dependencies:
|
||||
p-locate: 6.0.0
|
||||
|
||||
lodash-es@4.17.21: {}
|
||||
|
||||
lodash.castarray@4.4.0: {}
|
||||
|
||||
lodash.defaults@4.2.0: {}
|
||||
@ -8851,6 +8933,15 @@ snapshots:
|
||||
'@babel/types': 7.27.0
|
||||
source-map-js: 1.2.1
|
||||
|
||||
markdown-it@14.1.0:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
entities: 4.5.0
|
||||
linkify-it: 5.0.0
|
||||
mdurl: 2.0.0
|
||||
punycode.js: 2.3.1
|
||||
uc.micro: 2.1.0
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
mdn-data@2.0.28: {}
|
||||
@ -8859,6 +8950,8 @@ snapshots:
|
||||
|
||||
mdn-data@2.12.2: {}
|
||||
|
||||
mdurl@2.0.0: {}
|
||||
|
||||
media-typer@0.3.0: {}
|
||||
|
||||
merge-stream@2.0.0: {}
|
||||
@ -9117,15 +9210,23 @@ snapshots:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
||||
nuxt@3.16.1(@parcel/watcher@2.5.1)(db0@0.3.1)(eslint@9.23.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.37.0)(terser@5.39.0)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0):
|
||||
nuxt-lodash@2.5.3(magicast@0.3.5):
|
||||
dependencies:
|
||||
'@nuxt/kit': 3.16.1(magicast@0.3.5)
|
||||
'@types/lodash-es': 4.17.12
|
||||
lodash-es: 4.17.21
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
nuxt@3.16.1(@parcel/watcher@2.5.1)(db0@0.3.1)(eslint@9.23.0(jiti@2.4.2))(ioredis@5.6.0)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.37.0)(terser@5.39.0)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(yaml@2.7.1):
|
||||
dependencies:
|
||||
'@nuxt/cli': 3.23.1(magicast@0.3.5)
|
||||
'@nuxt/devalue': 2.0.2
|
||||
'@nuxt/devtools': 2.3.2(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2))
|
||||
'@nuxt/devtools': 2.3.2(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.2))
|
||||
'@nuxt/kit': 3.16.1(magicast@0.3.5)
|
||||
'@nuxt/schema': 3.16.1
|
||||
'@nuxt/telemetry': 2.6.6(magicast@0.3.5)
|
||||
'@nuxt/vite-builder': 3.16.1(eslint@9.23.0(jiti@2.4.2))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.37.0)(terser@5.39.0)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))(yaml@2.7.0)
|
||||
'@nuxt/vite-builder': 3.16.1(eslint@9.23.0(jiti@2.4.2))(magicast@0.3.5)(optionator@0.9.4)(rollup@4.37.0)(terser@5.39.0)(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))(yaml@2.7.1)
|
||||
'@oxc-parser/wasm': 0.60.0
|
||||
'@unhead/vue': 2.0.2(vue@3.5.13(typescript@5.8.2))
|
||||
'@vue/shared': 3.5.13
|
||||
@ -9728,6 +9829,8 @@ snapshots:
|
||||
once: 1.4.0
|
||||
optional: true
|
||||
|
||||
punycode.js@2.3.1: {}
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
quansync@0.2.10: {}
|
||||
@ -10434,6 +10537,8 @@ snapshots:
|
||||
|
||||
typescript@5.8.2: {}
|
||||
|
||||
uc.micro@2.1.0: {}
|
||||
|
||||
ufo@1.5.4: {}
|
||||
|
||||
ultrahtml@1.5.3: {}
|
||||
@ -10617,27 +10722,27 @@ snapshots:
|
||||
type-fest: 4.38.0
|
||||
vue: 3.5.13(typescript@5.8.2)
|
||||
|
||||
vite-dev-rpc@1.0.7(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)):
|
||||
vite-dev-rpc@1.0.7(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)):
|
||||
dependencies:
|
||||
birpc: 2.3.0
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)
|
||||
vite-hot-client: 2.0.4(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)
|
||||
vite-hot-client: 2.0.4(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))
|
||||
|
||||
vite-hot-client@0.2.4(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)):
|
||||
vite-hot-client@0.2.4(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)):
|
||||
dependencies:
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)
|
||||
|
||||
vite-hot-client@2.0.4(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)):
|
||||
vite-hot-client@2.0.4(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)):
|
||||
dependencies:
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)
|
||||
|
||||
vite-node@3.0.9(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0):
|
||||
vite-node@3.0.9(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.0
|
||||
es-module-lexer: 1.6.0
|
||||
pathe: 2.0.3
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
@ -10652,13 +10757,13 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vite-node@3.1.1(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0):
|
||||
vite-node@3.1.1(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.0
|
||||
es-module-lexer: 1.6.0
|
||||
pathe: 2.0.3
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
@ -10673,7 +10778,7 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vite-plugin-checker@0.9.1(eslint@9.23.0(jiti@2.4.2))(optionator@0.9.4)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)):
|
||||
vite-plugin-checker@0.9.1(eslint@9.23.0(jiti@2.4.2))(optionator@0.9.4)(typescript@5.8.2)(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)):
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.26.2
|
||||
chokidar: 4.0.3
|
||||
@ -10683,14 +10788,14 @@ snapshots:
|
||||
strip-ansi: 7.1.0
|
||||
tiny-invariant: 1.3.3
|
||||
tinyglobby: 0.2.12
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)
|
||||
vscode-uri: 3.1.0
|
||||
optionalDependencies:
|
||||
eslint: 9.23.0(jiti@2.4.2)
|
||||
optionator: 0.9.4
|
||||
typescript: 5.8.2
|
||||
|
||||
vite-plugin-inspect@11.0.0(@nuxt/kit@3.16.1(magicast@0.3.5))(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)):
|
||||
vite-plugin-inspect@11.0.0(@nuxt/kit@3.16.1(magicast@0.3.5))(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)):
|
||||
dependencies:
|
||||
ansis: 3.17.0
|
||||
debug: 4.4.0
|
||||
@ -10700,24 +10805,24 @@ snapshots:
|
||||
perfect-debounce: 1.0.0
|
||||
sirv: 3.0.1
|
||||
unplugin-utils: 0.2.4
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)
|
||||
vite-dev-rpc: 1.0.7(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)
|
||||
vite-dev-rpc: 1.0.7(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))
|
||||
optionalDependencies:
|
||||
'@nuxt/kit': 3.16.1(magicast@0.3.5)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-tracer@0.1.3(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(vue@3.5.13(typescript@5.8.2)):
|
||||
vite-plugin-vue-tracer@0.1.3(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.2)):
|
||||
dependencies:
|
||||
estree-walker: 3.0.3
|
||||
exsolve: 1.0.4
|
||||
magic-string: 0.30.17
|
||||
pathe: 2.0.3
|
||||
source-map-js: 1.2.1
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)
|
||||
vue: 3.5.13(typescript@5.8.2)
|
||||
|
||||
vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0):
|
||||
vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1):
|
||||
dependencies:
|
||||
esbuild: 0.25.1
|
||||
postcss: 8.5.3
|
||||
@ -10726,11 +10831,11 @@ snapshots:
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.4.2
|
||||
terser: 5.39.0
|
||||
yaml: 2.7.0
|
||||
yaml: 2.7.1
|
||||
|
||||
vitest-environment-nuxt@1.0.1(@testing-library/vue@8.1.0(@vue/compiler-sfc@3.5.13)(vue@3.5.13(typescript@5.8.2)))(@vue/test-utils@2.4.6)(happy-dom@17.4.4)(jiti@2.4.2)(magicast@0.3.5)(playwright-core@1.52.0)(terser@5.39.0)(typescript@5.8.2)(vitest@3.1.1(happy-dom@17.4.4)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0):
|
||||
vitest-environment-nuxt@1.0.1(@testing-library/vue@8.1.0(@vue/compiler-sfc@3.5.13)(vue@3.5.13(typescript@5.8.2)))(@vue/test-utils@2.4.6)(happy-dom@17.4.4)(jiti@2.4.2)(magicast@0.3.5)(playwright-core@1.52.0)(terser@5.39.0)(typescript@5.8.2)(vitest@3.1.1(happy-dom@17.4.4)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(yaml@2.7.1):
|
||||
dependencies:
|
||||
'@nuxt/test-utils': 3.17.2(@testing-library/vue@8.1.0(@vue/compiler-sfc@3.5.13)(vue@3.5.13(typescript@5.8.2)))(@vue/test-utils@2.4.6)(happy-dom@17.4.4)(jiti@2.4.2)(magicast@0.3.5)(playwright-core@1.52.0)(terser@5.39.0)(typescript@5.8.2)(vitest@3.1.1(happy-dom@17.4.4)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0)
|
||||
'@nuxt/test-utils': 3.17.2(@testing-library/vue@8.1.0(@vue/compiler-sfc@3.5.13)(vue@3.5.13(typescript@5.8.2)))(@vue/test-utils@2.4.6)(happy-dom@17.4.4)(jiti@2.4.2)(magicast@0.3.5)(playwright-core@1.52.0)(terser@5.39.0)(typescript@5.8.2)(vitest@3.1.1(happy-dom@17.4.4)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))(yaml@2.7.1)
|
||||
transitivePeerDependencies:
|
||||
- '@cucumber/cucumber'
|
||||
- '@jest/globals'
|
||||
@ -10756,10 +10861,10 @@ snapshots:
|
||||
- vitest
|
||||
- yaml
|
||||
|
||||
vitest@3.1.1(happy-dom@17.4.4)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0):
|
||||
vitest@3.1.1(happy-dom@17.4.4)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1):
|
||||
dependencies:
|
||||
'@vitest/expect': 3.1.1
|
||||
'@vitest/mocker': 3.1.1(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))
|
||||
'@vitest/mocker': 3.1.1(vite@6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1))
|
||||
'@vitest/pretty-format': 3.1.1
|
||||
'@vitest/runner': 3.1.1
|
||||
'@vitest/snapshot': 3.1.1
|
||||
@ -10775,8 +10880,8 @@ snapshots:
|
||||
tinyexec: 0.3.2
|
||||
tinypool: 1.0.2
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)
|
||||
vite-node: 3.1.1(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)
|
||||
vite: 6.2.3(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)
|
||||
vite-node: 3.1.1(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.1)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
happy-dom: 17.4.4
|
||||
@ -10923,6 +11028,9 @@ snapshots:
|
||||
|
||||
yaml@2.7.0: {}
|
||||
|
||||
yaml@2.7.1:
|
||||
optional: true
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yargs@17.7.2:
|
||||
|
@ -4,71 +4,93 @@ export default {
|
||||
darkMode: ['class'],
|
||||
content: [],
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
'3xl': '1792px',
|
||||
'4xl': '2048px',
|
||||
'5xl': '2560px',
|
||||
'6xl': '3840px',
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
colors: {
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
chart: {
|
||||
1: 'hsl(var(--chart-1))',
|
||||
2: 'hsl(var(--chart-2))',
|
||||
3: 'hsl(var(--chart-3))',
|
||||
4: 'hsl(var(--chart-4))',
|
||||
5: 'hsl(var(--chart-5))',
|
||||
},
|
||||
sidebar: {
|
||||
DEFAULT: 'hsl(var(--sidebar-background))',
|
||||
foreground: 'hsl(var(--sidebar-foreground))',
|
||||
primary: 'hsl(var(--sidebar-primary))',
|
||||
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
||||
accent: 'hsl(var(--sidebar-accent))',
|
||||
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
||||
border: 'hsl(var(--sidebar-border))',
|
||||
ring: 'hsl(var(--sidebar-ring))',
|
||||
},
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
screens: {
|
||||
'3xl': '1792px',
|
||||
'4xl': '2048px',
|
||||
'5xl': '2560px',
|
||||
'6xl': '3840px'
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
},
|
||||
colors: {
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))'
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))'
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))'
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))'
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))'
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))'
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))'
|
||||
},
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
chart: {
|
||||
'1': 'hsl(var(--chart-1))',
|
||||
'2': 'hsl(var(--chart-2))',
|
||||
'3': 'hsl(var(--chart-3))',
|
||||
'4': 'hsl(var(--chart-4))',
|
||||
'5': 'hsl(var(--chart-5))'
|
||||
},
|
||||
sidebar: {
|
||||
DEFAULT: 'hsl(var(--sidebar-background))',
|
||||
foreground: 'hsl(var(--sidebar-foreground))',
|
||||
primary: 'hsl(var(--sidebar-primary))',
|
||||
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
||||
accent: 'hsl(var(--sidebar-accent))',
|
||||
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
||||
border: 'hsl(var(--sidebar-border))',
|
||||
ring: 'hsl(var(--sidebar-ring))'
|
||||
}
|
||||
},
|
||||
keyframes: {
|
||||
'accordion-down': {
|
||||
from: {
|
||||
height: '0'
|
||||
},
|
||||
to: {
|
||||
height: 'var(--reka-accordion-content-height)'
|
||||
}
|
||||
},
|
||||
'accordion-up': {
|
||||
from: {
|
||||
height: 'var(--reka-accordion-content-height)'
|
||||
},
|
||||
to: {
|
||||
height: '0'
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')],
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user