docs-please 0.2.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +63 -0
  2. package/app/app.config.ts +13 -0
  3. package/app/app.vue +17 -0
  4. package/app/assets/css/main.css +367 -0
  5. package/app/components/Icons.ts +163 -0
  6. package/app/components/app/AppFooter.vue +24 -0
  7. package/app/components/app/AppHeader.vue +58 -0
  8. package/app/components/content/BrowserFrame.vue +21 -0
  9. package/app/components/content/Callout.vue +80 -0
  10. package/app/components/content/Caution.vue +25 -0
  11. package/app/components/content/CodeBlockCommand.vue +92 -0
  12. package/app/components/content/CodeCollapsibleWrapper.vue +50 -0
  13. package/app/components/content/CodeTabs.vue +14 -0
  14. package/app/components/content/ComponentPreview.vue +71 -0
  15. package/app/components/content/ComponentsList.vue +24 -0
  16. package/app/components/content/CopyButton.vue +39 -0
  17. package/app/components/content/FeatureCard.vue +25 -0
  18. package/app/components/content/LinkedCard.vue +19 -0
  19. package/app/components/content/Note.vue +25 -0
  20. package/app/components/content/ProseA.vue +18 -0
  21. package/app/components/content/ProseBlockQuote.vue +8 -0
  22. package/app/components/content/ProseCode.vue +8 -0
  23. package/app/components/content/ProseH1.vue +7 -0
  24. package/app/components/content/ProseH2.vue +8 -0
  25. package/app/components/content/ProseH3.vue +9 -0
  26. package/app/components/content/ProseH4.vue +9 -0
  27. package/app/components/content/ProseH5.vue +7 -0
  28. package/app/components/content/ProseH6.vue +7 -0
  29. package/app/components/content/ProseHr.vue +6 -0
  30. package/app/components/content/ProseIcon.vue +32 -0
  31. package/app/components/content/ProseImg.vue +6 -0
  32. package/app/components/content/ProseLi.vue +8 -0
  33. package/app/components/content/ProseOl.vue +8 -0
  34. package/app/components/content/ProseP.vue +14 -0
  35. package/app/components/content/ProsePre.vue +80 -0
  36. package/app/components/content/ProseStrong.vue +8 -0
  37. package/app/components/content/ProseTable.vue +26 -0
  38. package/app/components/content/ProseTd.vue +8 -0
  39. package/app/components/content/ProseTh.vue +8 -0
  40. package/app/components/content/ProseTr.vue +8 -0
  41. package/app/components/content/ProseUl.vue +8 -0
  42. package/app/components/content/Step.vue +18 -0
  43. package/app/components/content/Steps.vue +18 -0
  44. package/app/components/content/Tabs.vue +129 -0
  45. package/app/components/content/TabsItem.vue +26 -0
  46. package/app/components/content/Tip.vue +25 -0
  47. package/app/components/content/UButton.vue +34 -0
  48. package/app/components/content/UColorModeImage.vue +48 -0
  49. package/app/components/content/UPageCard.vue +83 -0
  50. package/app/components/content/UPageGrid.vue +18 -0
  51. package/app/components/content/UPageHero.vue +92 -0
  52. package/app/components/content/UPageSection.vue +90 -0
  53. package/app/components/content/Warning.vue +25 -0
  54. package/app/components/docs/DocsPageHeader.vue +20 -0
  55. package/app/components/docs/DocsPageNav.vue +31 -0
  56. package/app/components/docs/DocsSidebar.vue +97 -0
  57. package/app/components/docs/DocsTableOfContents.vue +64 -0
  58. package/app/components/ui/accordion/Accordion.vue +22 -0
  59. package/app/components/ui/accordion/AccordionContent.vue +23 -0
  60. package/app/components/ui/accordion/AccordionItem.vue +24 -0
  61. package/app/components/ui/accordion/AccordionTrigger.vue +37 -0
  62. package/app/components/ui/accordion/index.ts +4 -0
  63. package/app/components/ui/alert/Alert.vue +21 -0
  64. package/app/components/ui/alert/AlertDescription.vue +17 -0
  65. package/app/components/ui/alert/AlertTitle.vue +17 -0
  66. package/app/components/ui/alert/index.ts +28 -0
  67. package/app/components/ui/button/Button.vue +29 -0
  68. package/app/components/ui/button/index.ts +38 -0
  69. package/app/components/ui/card/Card.vue +22 -0
  70. package/app/components/ui/card/CardAction.vue +17 -0
  71. package/app/components/ui/card/CardContent.vue +17 -0
  72. package/app/components/ui/card/CardDescription.vue +17 -0
  73. package/app/components/ui/card/CardFooter.vue +17 -0
  74. package/app/components/ui/card/CardHeader.vue +17 -0
  75. package/app/components/ui/card/CardTitle.vue +17 -0
  76. package/app/components/ui/card/index.ts +7 -0
  77. package/app/components/ui/collapsible/Collapsible.vue +19 -0
  78. package/app/components/ui/collapsible/CollapsibleContent.vue +15 -0
  79. package/app/components/ui/collapsible/CollapsibleTrigger.vue +15 -0
  80. package/app/components/ui/collapsible/index.ts +3 -0
  81. package/app/components/ui/separator/Separator.vue +29 -0
  82. package/app/components/ui/separator/index.ts +1 -0
  83. package/app/components/ui/switch/Switch.vue +35 -0
  84. package/app/components/ui/switch/index.ts +1 -0
  85. package/app/components/ui/table/Table.vue +16 -0
  86. package/app/components/ui/table/TableBody.vue +14 -0
  87. package/app/components/ui/table/TableCaption.vue +14 -0
  88. package/app/components/ui/table/TableCell.vue +21 -0
  89. package/app/components/ui/table/TableEmpty.vue +34 -0
  90. package/app/components/ui/table/TableFooter.vue +14 -0
  91. package/app/components/ui/table/TableHead.vue +14 -0
  92. package/app/components/ui/table/TableHeader.vue +14 -0
  93. package/app/components/ui/table/TableRow.vue +14 -0
  94. package/app/components/ui/table/index.ts +9 -0
  95. package/app/components/ui/tabs/Tabs.vue +15 -0
  96. package/app/components/ui/tabs/TabsContent.vue +20 -0
  97. package/app/components/ui/tabs/TabsList.vue +23 -0
  98. package/app/components/ui/tabs/TabsTrigger.vue +27 -0
  99. package/app/components/ui/tabs/index.ts +4 -0
  100. package/app/components/ui/tooltip/Tooltip.vue +19 -0
  101. package/app/components/ui/tooltip/TooltipContent.vue +34 -0
  102. package/app/components/ui/tooltip/TooltipProvider.vue +14 -0
  103. package/app/components/ui/tooltip/TooltipTrigger.vue +15 -0
  104. package/app/components/ui/tooltip/index.ts +4 -0
  105. package/app/composables/useConfig.ts +24 -0
  106. package/app/composables/useNavigation.ts +43 -0
  107. package/app/layouts/default.vue +12 -0
  108. package/app/layouts/docs.vue +27 -0
  109. package/app/lib/utils.ts +7 -0
  110. package/app/pages/[...slug].vue +97 -0
  111. package/app/pages/index.vue +29 -0
  112. package/app/plugins/ssr-width.ts +5 -0
  113. package/content.config.ts +36 -0
  114. package/i18n/locales/en.json +14 -0
  115. package/modules/config.ts +38 -0
  116. package/modules/css.ts +38 -0
  117. package/modules/shadcn.ts +116 -0
  118. package/nuxt.config.ts +125 -0
  119. package/nuxt.schema.ts +68 -0
  120. package/package.json +81 -0
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ import type { Component, HTMLAttributes } from 'vue'
3
+ </script>
4
+
5
+ <script setup lang="ts">
6
+ import { useForwardProps } from 'reka-ui'
7
+ import { reactivePick } from '@vueuse/core'
8
+
9
+ export interface ProseIconProps {
10
+ name: string | Component
11
+ mode?: 'svg' | 'css'
12
+ size?: string | number
13
+ class?: HTMLAttributes['class']
14
+ }
15
+
16
+ const props = defineProps<ProseIconProps>()
17
+
18
+ const iconProps = useForwardProps(reactivePick(props, 'name', 'mode', 'size'))
19
+ </script>
20
+
21
+ <template>
22
+ <Icon
23
+ v-if="typeof name === 'string'"
24
+ v-bind="iconProps"
25
+ :class="props.class"
26
+ />
27
+ <component
28
+ :is="name"
29
+ v-else
30
+ :class="props.class"
31
+ />
32
+ </template>
@@ -0,0 +1,6 @@
1
+ <script setup lang="ts">
2
+ </script>
3
+
4
+ <template>
5
+ <img class="rounded-md">
6
+ </template>
@@ -0,0 +1,8 @@
1
+ <script setup lang="ts">
2
+ </script>
3
+
4
+ <template>
5
+ <li class="mt-2">
6
+ <slot />
7
+ </li>
8
+ </template>
@@ -0,0 +1,8 @@
1
+ <script setup lang="ts">
2
+ </script>
3
+
4
+ <template>
5
+ <ol class="my-6 ml-6 list-decimal">
6
+ <slot />
7
+ </ol>
8
+ </template>
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+ import { cn } from '~/lib/utils'
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes['class']
7
+ }>()
8
+ </script>
9
+
10
+ <template>
11
+ <p :class="cn('leading-relaxed [&:not(:first-child)]:mt-6', props.class)">
12
+ <slot />
13
+ </p>
14
+ </template>
@@ -0,0 +1,80 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+ import { getIconForLanguageExtension } from '~/components/Icons'
4
+ import { cn } from '~/lib/utils'
5
+
6
+ const props = defineProps<{
7
+ code?: string
8
+ language?: string
9
+ filename?: string
10
+ highlights?: number[]
11
+ meta?: string
12
+ class?: HTMLAttributes['class']
13
+ unwrap?: boolean
14
+ }>()
15
+
16
+ const npmBlock = ['npm install', 'npm create', 'npm run', 'npx']
17
+ const isNpmCommand = computed(() => props.code && npmBlock.some(s => props.code!.startsWith(s)))
18
+ const isShowingLineNumber = computed(() => props.meta?.includes('showLineNumbers'))
19
+
20
+ const title = computed(() => props.filename || props.meta?.match(/title="([^"]+)"/)?.[1])
21
+
22
+ const lang = computed(() => props.language?.replace('language-', '') || '')
23
+ const IconExtension = computed(() => {
24
+ return getIconForLanguageExtension(lang.value)
25
+ })
26
+
27
+ const codeAttributes = computed(() => isShowingLineNumber.value
28
+ ? ({
29
+ 'data-line-numbers': '',
30
+ 'data-line-numbers-max-digits': 2,
31
+ })
32
+ : undefined)
33
+ </script>
34
+
35
+ <template>
36
+ <pre
37
+ v-if="unwrap"
38
+ :class="cn('no-scrollbar min-w-0 overflow-x-auto px-4 py-3.5 outline-none has-[[data-highlighted-line]]:px-0 has-[[data-line-numbers]]:px-0 has-[[data-slot=tabs]]:p-0 !bg-transparent', props.class)"
39
+ :data-language="lang"
40
+ ><code v-bind="codeAttributes"><slot /></code></pre>
41
+ <figure
42
+ v-else
43
+ data-pretty-code-figure
44
+ >
45
+ <pre
46
+ v-if="isNpmCommand"
47
+ :class="cn('no-scrollbar min-w-0 overflow-x-auto px-4 py-3.5 outline-none has-[[data-highlighted-line]]:px-0 has-[[data-line-numbers]]:px-0 has-[[data-slot=tabs]]:p-0', props.class)"
48
+ ><CodeBlockCommand :code="code!" /></pre>
49
+
50
+ <template v-else-if="title">
51
+ <figcaption
52
+ data-pretty-code-title
53
+ :data-language="lang"
54
+ class="text-code-foreground [&_svg]:text-code-foreground flex items-center gap-2 [&_svg]:size-4 [&_svg]:opacity-70"
55
+ >
56
+ <component
57
+ :is="IconExtension"
58
+ v-if="IconExtension"
59
+ />
60
+ {{ title }}
61
+ </figcaption>
62
+ <pre
63
+ :data-language="lang"
64
+ :class="cn('relative no-scrollbar min-w-0 overflow-x-auto px-4 py-3.5 outline-none has-[[data-highlighted-line]]:px-0 has-[[data-line-numbers]]:px-0 has-[[data-slot=tabs]]:p-0', props.class)"
65
+ ><CopyButton
66
+ v-if="code"
67
+ :value="code"
68
+ /><code v-bind="codeAttributes"><slot /></code></pre>
69
+ </template>
70
+
71
+ <pre
72
+ v-else
73
+ :data-language="lang"
74
+ :class="cn('relative no-scrollbar min-w-0 overflow-x-auto px-4 py-3.5 outline-none has-[[data-highlighted-line]]:px-0 has-[[data-line-numbers]]:px-0 has-[[data-slot=tabs]]:p-0', props.class)"
75
+ ><CopyButton
76
+ v-if="code"
77
+ :value="code"
78
+ /><code v-bind="codeAttributes"><slot /></code></pre>
79
+ </figure>
80
+ </template>
@@ -0,0 +1,8 @@
1
+ <script setup lang="ts">
2
+ </script>
3
+
4
+ <template>
5
+ <strong class="font-medium">
6
+ <slot />
7
+ </strong>
8
+ </template>
@@ -0,0 +1,26 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+ import { cn } from '~/lib/utils'
4
+
5
+ defineOptions({
6
+ inheritAttrs: false,
7
+ })
8
+
9
+ const props = defineProps<{
10
+ class?: HTMLAttributes['class']
11
+ }>()
12
+ </script>
13
+
14
+ <template>
15
+ <div class="no-scrollbar my-6 w-full overflow-y-auto rounded-lg border">
16
+ <table
17
+ :class="cn(
18
+ 'relative w-full overflow-hidden border-none text-sm [&_tbody_tr:last-child]:border-b-0',
19
+ props.class,
20
+ )"
21
+ v-bind="$attrs"
22
+ >
23
+ <slot />
24
+ </table>
25
+ </div>
26
+ </template>
@@ -0,0 +1,8 @@
1
+ <script setup lang="ts">
2
+ </script>
3
+
4
+ <template>
5
+ <td class="px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right">
6
+ <slot />
7
+ </td>
8
+ </template>
@@ -0,0 +1,8 @@
1
+ <script setup lang="ts">
2
+ </script>
3
+
4
+ <template>
5
+ <th class="px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right">
6
+ <slot />
7
+ </th>
8
+ </template>
@@ -0,0 +1,8 @@
1
+ <script setup lang="ts">
2
+ </script>
3
+
4
+ <template>
5
+ <tr class="last:border-b-none m-0 border-b">
6
+ <slot />
7
+ </tr>
8
+ </template>
@@ -0,0 +1,8 @@
1
+ <script setup lang="ts">
2
+ </script>
3
+
4
+ <template>
5
+ <ul class="my-6 ml-6 list-disc">
6
+ <slot />
7
+ </ul>
8
+ </template>
@@ -0,0 +1,18 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+ import { cn } from '~/lib/utils'
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes['class']
7
+ }>()
8
+ </script>
9
+
10
+ <template>
11
+ <h3
12
+ :class="cn(
13
+ 'font-heading mt-8 scroll-m-32 text-xl font-medium tracking-tight', props.class,
14
+ )"
15
+ >
16
+ <slot mdc-unwrap="p" />
17
+ </h3>
18
+ </template>
@@ -0,0 +1,18 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+ import { cn } from '~/lib/utils'
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes['class']
7
+ }>()
8
+ </script>
9
+
10
+ <template>
11
+ <div
12
+ :class="cn(
13
+ '[&>h3]:step steps mb-12 [counter-reset:step] *:[h3]:first:!mt-0', props.class,
14
+ )"
15
+ >
16
+ <slot />
17
+ </div>
18
+ </template>
@@ -0,0 +1,129 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes, VNode } from 'vue'
3
+ import { computed, onBeforeUpdate, onMounted, ref, watch } from 'vue'
4
+ import { cn } from '~/lib/utils'
5
+ import {
6
+ Tabs as UITabs,
7
+ TabsContent as UITabsContent,
8
+ TabsList as UITabsList,
9
+ TabsTrigger as UITabsTrigger,
10
+ } from '~/components/ui/tabs'
11
+
12
+ export interface TabsProps {
13
+ /**
14
+ * Default selected tab index (0-based).
15
+ * @default 0
16
+ */
17
+ defaultIndex?: number
18
+ /**
19
+ * Sync selected tab across all tabs with this key via localStorage.
20
+ * @example 'package-manager'
21
+ */
22
+ sync?: string
23
+ class?: HTMLAttributes['class']
24
+ }
25
+
26
+ const props = withDefaults(defineProps<TabsProps>(), {
27
+ defaultIndex: 0,
28
+ })
29
+
30
+ const slots = defineSlots<{
31
+ default(): VNode[]
32
+ }>()
33
+
34
+ const defaultValue = computed(() => String(props.defaultIndex))
35
+ const model = ref<string>(defaultValue.value)
36
+ const rerenderCount = ref(1)
37
+
38
+ interface TabItem {
39
+ value: string
40
+ label: string
41
+ icon?: string
42
+ component: VNode
43
+ }
44
+
45
+ const items = computed<TabItem[]>(() => {
46
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
47
+ rerenderCount.value
48
+
49
+ let counter = 0
50
+ function transformSlot(slot: VNode): TabItem | null {
51
+ if (typeof slot.type === 'symbol') {
52
+ return null // Fragments are handled by flatMap in parent call
53
+ }
54
+
55
+ const slotProps = slot.props as { label?: string, icon?: string } | null
56
+ if (!slotProps?.label) {
57
+ return null
58
+ }
59
+
60
+ return {
61
+ value: String(counter++),
62
+ label: slotProps.label,
63
+ icon: slotProps.icon,
64
+ component: slot,
65
+ }
66
+ }
67
+
68
+ function flattenSlots(slot: VNode): VNode[] {
69
+ if (typeof slot.type === 'symbol') {
70
+ const children = slot.children as VNode[] | null
71
+ return children?.flatMap(flattenSlots) ?? []
72
+ }
73
+ return [slot]
74
+ }
75
+
76
+ return slots.default?.()?.flatMap(flattenSlots).map(transformSlot).filter((item): item is TabItem => item !== null) || []
77
+ })
78
+
79
+ onMounted(() => {
80
+ if (props.sync) {
81
+ const syncKey = `tabs-${props.sync}`
82
+ const stored = localStorage.getItem(syncKey)
83
+
84
+ if (stored && items.value.some(item => item.value === stored)) {
85
+ model.value = stored
86
+ }
87
+
88
+ watch(model, (value) => {
89
+ if (value) {
90
+ localStorage.setItem(syncKey, value)
91
+ }
92
+ })
93
+ }
94
+ })
95
+
96
+ onBeforeUpdate(() => rerenderCount.value++)
97
+ </script>
98
+
99
+ <template>
100
+ <UITabs
101
+ v-model="model"
102
+ :default-value="defaultValue"
103
+ :class="cn('relative mt-6', props.class)"
104
+ >
105
+ <UITabsList>
106
+ <UITabsTrigger
107
+ v-for="item in items"
108
+ :key="item.value"
109
+ :value="item.value"
110
+ >
111
+ <Icon
112
+ v-if="item.icon"
113
+ :name="item.icon"
114
+ class="size-4 mr-1.5"
115
+ aria-hidden="true"
116
+ />
117
+ {{ item.label }}
118
+ </UITabsTrigger>
119
+ </UITabsList>
120
+ <UITabsContent
121
+ v-for="item in items"
122
+ :key="item.value"
123
+ :value="item.value"
124
+ class="mt-2"
125
+ >
126
+ <component :is="item.component" />
127
+ </UITabsContent>
128
+ </UITabs>
129
+ </template>
@@ -0,0 +1,26 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes, VNode } from 'vue'
3
+
4
+ export interface TabsItemProps {
5
+ /**
6
+ * The label displayed in the tab trigger.
7
+ */
8
+ label: string
9
+ /**
10
+ * Optional icon name for the tab.
11
+ */
12
+ icon?: string
13
+ class?: HTMLAttributes['class']
14
+ }
15
+
16
+ const props = defineProps<TabsItemProps>()
17
+ defineSlots<{
18
+ default(): VNode[]
19
+ }>()
20
+ </script>
21
+
22
+ <template>
23
+ <div :class="props.class">
24
+ <slot />
25
+ </div>
26
+ </template>
@@ -0,0 +1,25 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+ import Callout from './Callout.vue'
4
+
5
+ defineProps<{
6
+ title?: string
7
+ class?: HTMLAttributes['class']
8
+ }>()
9
+ </script>
10
+
11
+ <template>
12
+ <Callout
13
+ type="tip"
14
+ :title
15
+ :class
16
+ >
17
+ <template
18
+ v-if="$slots.title"
19
+ #title
20
+ >
21
+ <slot name="title" />
22
+ </template>
23
+ <slot />
24
+ </Callout>
25
+ </template>
@@ -0,0 +1,34 @@
1
+ <script setup lang="ts">
2
+ import type { NuxtLinkProps } from '#app'
3
+ import type { ButtonVariants } from '~/components/ui/button'
4
+ import Button from '~/components/ui/button/Button.vue'
5
+
6
+ interface Props extends NuxtLinkProps {
7
+ variant?: ButtonVariants['variant']
8
+ size?: ButtonVariants['size']
9
+ }
10
+
11
+ const props = withDefaults(defineProps<Props>(), {
12
+ size: 'default',
13
+ variant: 'default',
14
+ })
15
+ </script>
16
+
17
+ <template>
18
+ <Button
19
+ :size="size"
20
+ :variant="variant"
21
+ as-child
22
+ class="no-underline"
23
+ >
24
+ <NuxtLink
25
+ :to="to"
26
+ :href="href"
27
+ :target="target"
28
+ :rel="rel"
29
+ :external="external"
30
+ >
31
+ <slot />
32
+ </NuxtLink>
33
+ </Button>
34
+ </template>
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ interface Props {
3
+ light: string
4
+ dark: string
5
+ alt?: string
6
+ width?: number | string
7
+ height?: number | string
8
+ class?: string
9
+ format?: string
10
+ loading?: 'lazy' | 'eager'
11
+ }
12
+
13
+ const props = withDefaults(defineProps<Props>(), {
14
+ alt: '',
15
+ loading: 'lazy',
16
+ })
17
+
18
+ const colorMode = useColorMode()
19
+
20
+ const src = computed(() => {
21
+ return colorMode.value === 'dark' ? props.dark : props.light
22
+ })
23
+ </script>
24
+
25
+ <template>
26
+ <ClientOnly>
27
+ <NuxtImg
28
+ :src="src"
29
+ :alt="alt"
30
+ :width="width"
31
+ :height="height"
32
+ :class="props.class"
33
+ :loading="loading"
34
+ :format="format"
35
+ />
36
+ <template #fallback>
37
+ <NuxtImg
38
+ :src="light"
39
+ :alt="alt"
40
+ :width="width"
41
+ :height="height"
42
+ :class="props.class"
43
+ :loading="loading"
44
+ :format="format"
45
+ />
46
+ </template>
47
+ </ClientOnly>
48
+ </template>
@@ -0,0 +1,83 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+ import type { NuxtLinkProps } from '#app'
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '~/components/ui/card'
5
+
6
+ const props = defineProps<{
7
+ title?: string
8
+ description?: string
9
+ icon?: string
10
+ to?: NuxtLinkProps['to']
11
+ target?: string
12
+ orientation?: 'vertical' | 'horizontal'
13
+ spotlight?: boolean
14
+ class?: HTMLAttributes['class']
15
+ }>()
16
+
17
+ const isLink = computed(() => !!props.to)
18
+ </script>
19
+
20
+ <template>
21
+ <Card
22
+ :class="[
23
+ 'transition-colors',
24
+ isLink && 'hover:border-primary/50 cursor-pointer',
25
+ props.spotlight && 'group',
26
+ props.class,
27
+ ]"
28
+ >
29
+ <component
30
+ :is="isLink ? 'NuxtLink' : 'div'"
31
+ :to="props.to"
32
+ :target="props.target"
33
+ :class="[
34
+ 'block h-full',
35
+ props.orientation === 'horizontal' && 'flex flex-col sm:flex-row',
36
+ ]"
37
+ >
38
+ <div :class="props.orientation === 'horizontal' && 'flex-1'">
39
+ <CardHeader v-if="$slots.title || $slots.description || props.title || props.description || props.icon">
40
+ <!-- Icon (decorative - title/description provide meaning) -->
41
+ <div
42
+ v-if="props.icon"
43
+ class="mb-2"
44
+ >
45
+ <Icon
46
+ :name="props.icon"
47
+ class="size-8 text-primary"
48
+ aria-hidden="true"
49
+ />
50
+ </div>
51
+
52
+ <!-- Title -->
53
+ <CardTitle v-if="$slots.title || props.title">
54
+ <slot name="title">
55
+ {{ props.title }}
56
+ </slot>
57
+ </CardTitle>
58
+
59
+ <!-- Description -->
60
+ <CardDescription v-if="$slots.description || props.description">
61
+ <slot name="description">
62
+ {{ props.description }}
63
+ </slot>
64
+ </CardDescription>
65
+ </CardHeader>
66
+
67
+ <CardContent v-if="$slots.body">
68
+ <slot name="body" />
69
+ </CardContent>
70
+ </div>
71
+
72
+ <!-- Default slot for media/illustration (below content) -->
73
+ <CardContent
74
+ v-if="$slots.default"
75
+ :class="[
76
+ props.orientation === 'horizontal' ? 'sm:w-1/3 shrink-0' : '',
77
+ ]"
78
+ >
79
+ <slot />
80
+ </CardContent>
81
+ </component>
82
+ </Card>
83
+ </template>
@@ -0,0 +1,18 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+
4
+ const props = defineProps<{
5
+ class?: HTMLAttributes['class']
6
+ }>()
7
+ </script>
8
+
9
+ <template>
10
+ <div
11
+ :class="[
12
+ 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6',
13
+ props.class,
14
+ ]"
15
+ >
16
+ <slot />
17
+ </div>
18
+ </template>