@lupinum/ginko-docs 0.2.2 → 0.2.3

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 (196) hide show
  1. package/README.md +130 -73
  2. package/app/app.config.ts +14 -12
  3. package/app/app.vue +2 -2
  4. package/app/assets/css/prose.css +2203 -149
  5. package/app/assets/css/tailwind.css +60 -38
  6. package/app/assets/css/theme-palettes.css +309 -0
  7. package/app/components/content/DocumentPageShell.vue +1 -1
  8. package/app/components/content/Feedback.vue +12 -11
  9. package/app/components/content/ImageZoomDialog.vue +84 -0
  10. package/app/components/content/PageMarkdownCopy.vue +2 -0
  11. package/app/components/content/imageZoom.ts +25 -0
  12. package/app/components/mdc/MdcAccordion.vue +6 -3
  13. package/app/components/mdc/MdcAccordionItem.vue +7 -16
  14. package/app/components/mdc/MdcApi.vue +159 -0
  15. package/app/components/mdc/MdcAside.vue +6 -10
  16. package/app/components/mdc/MdcCallout.vue +9 -10
  17. package/app/components/mdc/MdcCard.vue +36 -47
  18. package/app/components/mdc/MdcCards.vue +30 -0
  19. package/app/components/mdc/MdcCodeGroup.vue +4 -0
  20. package/app/components/mdc/MdcCodeTree.vue +234 -0
  21. package/app/components/mdc/MdcCollapse.vue +61 -0
  22. package/app/components/mdc/MdcError.vue +8 -1
  23. package/app/components/mdc/MdcExcerpt.vue +26 -0
  24. package/app/components/mdc/MdcFigure.vue +54 -20
  25. package/app/components/mdc/MdcFiles.vue +120 -0
  26. package/app/components/mdc/MdcIdea.vue +2 -1
  27. package/app/components/mdc/MdcInfo.vue +2 -1
  28. package/app/components/mdc/MdcInlineToc.vue +51 -0
  29. package/app/components/{ui/dialog/DialogHeader.vue → mdc/MdcKbd.vue} +2 -4
  30. package/app/components/mdc/MdcNote.vue +2 -1
  31. package/app/components/mdc/MdcQuiz.vue +43 -83
  32. package/app/components/mdc/MdcQuizQuestion.vue +104 -109
  33. package/app/components/mdc/MdcReadMore.vue +57 -77
  34. package/app/components/mdc/MdcSteps.vue +45 -11
  35. package/app/components/mdc/MdcSuccess.vue +8 -1
  36. package/app/components/mdc/MdcTabs.vue +44 -26
  37. package/app/components/mdc/MdcTimeline.vue +4 -1
  38. package/app/components/mdc/MdcTimelineItem.vue +18 -27
  39. package/app/components/mdc/MdcWarning.vue +8 -1
  40. package/app/components/mdc/api.utils.ts +132 -0
  41. package/app/components/mdc/code-tree.utils.ts +22 -0
  42. package/app/components/mdc/quiz.utils.ts +38 -0
  43. package/app/components/prose/ProseBlockquote.vue +17 -1
  44. package/app/components/prose/ProseH1.vue +1 -1
  45. package/app/components/prose/ProseH2.vue +7 -2
  46. package/app/components/prose/ProseH3.vue +7 -2
  47. package/app/components/prose/ProseH4.vue +17 -2
  48. package/app/components/{ui/kbd/KbdGroup.vue → prose/ProseH5.vue} +4 -3
  49. package/app/components/{ui/card/CardTitle.vue → prose/ProseH6.vue} +4 -3
  50. package/app/components/prose/ProseImg.vue +19 -112
  51. package/app/components/prose/ProsePre.vue +63 -100
  52. package/app/components/prose/ProseTable.vue +5 -3
  53. package/app/components/site/ModeToggle.vue +12 -3
  54. package/app/components/site/SiteBanner.vue +43 -2
  55. package/app/components/site/SiteFooter.vue +3 -5
  56. package/app/components/site/SiteHeader.vue +255 -132
  57. package/app/components/site/SiteHeroCode.vue +164 -0
  58. package/app/components/site/SiteLocaleSwitcher.vue +22 -4
  59. package/app/components/ui/badge/Badge.vue +1 -1
  60. package/app/components/ui/button/Button.vue +1 -1
  61. package/app/components/ui/collapsible/CollapsibleContent.vue +1 -1
  62. package/app/components/ui/dropdown-menu/DropdownMenuContent.vue +14 -4
  63. package/app/components/ui/dropdown-menu/DropdownMenuItem.vue +1 -1
  64. package/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +1 -1
  65. package/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue +1 -1
  66. package/app/components/ui/dropdown-menu/index.ts +0 -9
  67. package/app/components/ui/kbd/Kbd.vue +1 -1
  68. package/app/components/ui/kbd/index.ts +0 -1
  69. package/app/components/ui/scroll-area/ScrollArea.vue +1 -1
  70. package/app/components/ui/scroll-area/ScrollBar.vue +1 -1
  71. package/app/components/ui/separator/Separator.vue +1 -1
  72. package/app/components/ui/sheet/SheetContent.vue +1 -1
  73. package/app/components/ui/sheet/SheetDescription.vue +1 -1
  74. package/app/components/ui/sheet/SheetOverlay.vue +1 -1
  75. package/app/components/ui/sheet/SheetTitle.vue +1 -1
  76. package/app/components/ui/sheet/index.ts +0 -3
  77. package/app/components/ui/tabs/Tabs.vue +1 -1
  78. package/app/components/ui/tabs/TabsList.vue +1 -1
  79. package/app/components/ui/tabs/TabsTrigger.vue +1 -1
  80. package/app/components/ui/tabs/index.ts +0 -1
  81. package/app/composables/site-navigation.utils.ts +29 -11
  82. package/app/composables/useContentRouteAlternates.ts +13 -21
  83. package/app/composables/useLocalizedRouteSwitch.ts +1 -1
  84. package/app/composables/useMetaKey.ts +18 -0
  85. package/app/composables/useProseAppearance.ts +24 -0
  86. package/app/composables/useSiteNavigation.ts +3 -18
  87. package/app/config/site.utils.ts +1 -1
  88. package/app/error.vue +1 -5
  89. package/app/features/docs/components/DocsBreadcrumb.vue +37 -0
  90. package/app/features/docs/components/DocsContributeLinks.vue +28 -18
  91. package/app/features/docs/components/DocsMobileToc.vue +12 -8
  92. package/app/features/docs/components/DocsPageContent.vue +84 -47
  93. package/app/features/docs/components/DocsPageNav.vue +1 -1
  94. package/app/features/docs/components/DocsSidebar.vue +30 -20
  95. package/app/features/docs/components/DocsSidebarItem.vue +53 -100
  96. package/app/features/docs/components/DocsSidebarRow.vue +27 -0
  97. package/app/features/docs/components/DocsSidebarTabs.vue +46 -22
  98. package/app/features/docs/components/DocsToc.vue +80 -34
  99. package/app/features/docs/composables/useDocsNavigation.ts +2 -2
  100. package/app/features/docs/composables/useDocsNavigationData.ts +4 -1
  101. package/app/features/docs/composables/useRevealActive.ts +58 -0
  102. package/app/features/docs/composables/useScrollspy.ts +87 -19
  103. package/app/features/docs/docs-navigation.ts +17 -30
  104. package/app/features/docs/toc-context.ts +6 -0
  105. package/app/features/search/command-center.ts +11 -90
  106. package/app/features/search/components/SiteCommandCenter.vue +58 -38
  107. package/app/features/search/useCommandCenter.ts +46 -46
  108. package/app/layouts/docs.vue +1 -1
  109. package/app/lib/errors.ts +2 -49
  110. package/app/pages/blog/[slug].vue +16 -8
  111. package/app/pages/blog/index.vue +26 -13
  112. package/app/pages/docs/index.vue +10 -14
  113. package/app/pages/index.vue +174 -59
  114. package/app/plugins/ginko-docs-theme.ts +12 -0
  115. package/app/utils/content.ts +11 -0
  116. package/app/utils/file-icons.ts +90 -0
  117. package/app/utils/index.ts +7 -1
  118. package/app/utils/repository.ts +20 -0
  119. package/content.js +23 -26
  120. package/content.ts +30 -28
  121. package/i18n/messages/global/blog.ts +4 -0
  122. package/i18n/messages/global/command.ts +6 -2
  123. package/i18n/messages/global/docs.ts +3 -0
  124. package/i18n/messages/global/nav.ts +6 -2
  125. package/i18n/messages/index.ts +3 -3
  126. package/icon-bundle.ts +5 -0
  127. package/index.d.ts +7 -1
  128. package/nuxt.config.ts +2 -2
  129. package/package.json +9 -6
  130. package/runtime/server/agent-markdown.ts +0 -2
  131. package/shared/theme-palettes.ts +37 -0
  132. package/shared/types/app-config.ts +63 -6
  133. package/tags.ts +47 -59
  134. package/app/components/mdc/MdcAlert.vue +0 -34
  135. package/app/components/mdc/MdcCardGroup.vue +0 -47
  136. package/app/components/mdc/MdcDocImg.vue +0 -25
  137. package/app/components/mdc/MdcField.vue +0 -62
  138. package/app/components/mdc/MdcFieldGroup.vue +0 -18
  139. package/app/components/mdc/MdcPassage.vue +0 -32
  140. package/app/components/mdc/MdcQuizOption.vue +0 -91
  141. package/app/components/mdc/MdcReadMoreGroup.vue +0 -18
  142. package/app/components/mdc/MdcShortcut.vue +0 -46
  143. package/app/components/mdc/MdcStep.vue +0 -42
  144. package/app/components/ui/accordion/Accordion.vue +0 -15
  145. package/app/components/ui/accordion/AccordionContent.vue +0 -23
  146. package/app/components/ui/accordion/AccordionItem.vue +0 -24
  147. package/app/components/ui/accordion/AccordionTrigger.vue +0 -35
  148. package/app/components/ui/accordion/index.ts +0 -4
  149. package/app/components/ui/alert/Alert.vue +0 -21
  150. package/app/components/ui/alert/AlertDescription.vue +0 -22
  151. package/app/components/ui/alert/AlertTitle.vue +0 -22
  152. package/app/components/ui/alert/index.ts +0 -34
  153. package/app/components/ui/card/Card.vue +0 -16
  154. package/app/components/ui/card/CardAction.vue +0 -17
  155. package/app/components/ui/card/CardContent.vue +0 -14
  156. package/app/components/ui/card/CardDescription.vue +0 -14
  157. package/app/components/ui/card/CardFooter.vue +0 -14
  158. package/app/components/ui/card/CardHeader.vue +0 -22
  159. package/app/components/ui/card/card-variants.ts +0 -15
  160. package/app/components/ui/card/index.ts +0 -8
  161. package/app/components/ui/checkbox/Checkbox.vue +0 -35
  162. package/app/components/ui/checkbox/index.ts +0 -1
  163. package/app/components/ui/dialog/Dialog.vue +0 -15
  164. package/app/components/ui/dialog/DialogClose.vue +0 -12
  165. package/app/components/ui/dialog/DialogContent.vue +0 -69
  166. package/app/components/ui/dialog/DialogDescription.vue +0 -21
  167. package/app/components/ui/dialog/DialogFooter.vue +0 -15
  168. package/app/components/ui/dialog/DialogOverlay.vue +0 -26
  169. package/app/components/ui/dialog/DialogPortal.vue +0 -12
  170. package/app/components/ui/dialog/DialogTitle.vue +0 -21
  171. package/app/components/ui/dialog/DialogTrigger.vue +0 -12
  172. package/app/components/ui/dialog/index.ts +0 -10
  173. package/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +0 -36
  174. package/app/components/ui/dropdown-menu/DropdownMenuGroup.vue +0 -12
  175. package/app/components/ui/dropdown-menu/DropdownMenuLabel.vue +0 -25
  176. package/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue +0 -17
  177. package/app/components/ui/dropdown-menu/DropdownMenuSub.vue +0 -15
  178. package/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue +0 -29
  179. package/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +0 -31
  180. package/app/components/ui/input/Input.vue +0 -23
  181. package/app/components/ui/input/index.ts +0 -1
  182. package/app/components/ui/label/Label.vue +0 -14
  183. package/app/components/ui/label/index.ts +0 -1
  184. package/app/components/ui/sheet/SheetClose.vue +0 -12
  185. package/app/components/ui/sheet/SheetFooter.vue +0 -12
  186. package/app/components/ui/sheet/SheetHeader.vue +0 -12
  187. package/app/components/ui/switch/Switch.vue +0 -39
  188. package/app/components/ui/switch/index.ts +0 -1
  189. package/app/components/ui/tabs/TabsContent.vue +0 -21
  190. package/app/components/ui/textarea/Textarea.vue +0 -31
  191. package/app/components/ui/textarea/index.ts +0 -1
  192. package/app/features/docs/composables/useDocsEntryPath.ts +0 -13
  193. package/app/lib/utils.ts +0 -7
  194. package/i18n/messages/source.ts +0 -5
  195. package/runtime/markdown/inline-code-highlight.ts +0 -155
  196. package/runtime/utils.ts +0 -7
@@ -2,6 +2,7 @@
2
2
  import { onMounted, ref } from "vue";
3
3
  import { AccordionRoot } from "reka-ui";
4
4
  import { cn } from "../../utils";
5
+ import { useProseAppearance } from "../../composables/useProseAppearance";
5
6
 
6
7
  const props = withDefaults(
7
8
  defineProps<{
@@ -9,12 +10,14 @@ const props = withDefaults(
9
10
  collapsible?: boolean;
10
11
  defaultValue?: string | string[];
11
12
  class?: string;
13
+ appearance?: "quiet" | "tint";
12
14
  }>(),
13
15
  {
14
16
  type: "single",
15
17
  collapsible: true,
16
18
  },
17
19
  );
20
+ const appearance = useProseAppearance("accordion", () => props.appearance);
18
21
 
19
22
  const root = ref<HTMLElement | null>(null);
20
23
  const value = ref<string | string[]>(
@@ -53,10 +56,10 @@ onMounted(() => {
53
56
  v-model="value"
54
57
  :type="props.type"
55
58
  :collapsible="props.collapsible"
59
+ :unmount-on-hide="false"
56
60
  data-slot="accordion"
57
- :class="
58
- cn('not-prose my-4 w-full divide-y overflow-hidden rounded-lg border bg-card', props.class)
59
- "
61
+ :data-appearance="appearance"
62
+ :class="cn('content-accordion not-prose', props.class)"
60
63
  >
61
64
  <slot />
62
65
  </AccordionRoot>
@@ -15,35 +15,26 @@ const autoValue = useId();
15
15
  <AccordionItem
16
16
  :value="props.value || autoValue"
17
17
  :data-accordion-value="props.value || autoValue"
18
- class="border-b last:border-b-0"
18
+ class="content-accordion-item"
19
19
  >
20
- <AccordionHeader class="flex">
21
- <AccordionTrigger
22
- class="flex flex-1 items-start justify-between gap-4 rounded-md px-4 py-3 text-left text-sm font-medium transition-all outline-none hover:no-underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180"
23
- >
20
+ <AccordionHeader class="content-accordion-header">
21
+ <AccordionTrigger class="content-accordion-trigger">
24
22
  <slot name="title">
25
23
  {{ title }}
26
24
  </slot>
27
- <Icon
28
- name="lucide:chevron-down"
29
- class="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200"
30
- aria-hidden="true"
31
- />
25
+ <Icon name="lucide:chevron-down" class="content-accordion-chevron" aria-hidden="true" />
32
26
  </AccordionTrigger>
33
27
  </AccordionHeader>
34
28
  <AccordionContent
35
29
  data-slot="accordion-content"
36
- class="accordion-content overflow-hidden text-sm"
30
+ class="content-accordion-content accordion-content"
37
31
  >
38
32
  <slot name="content">
39
- <div v-if="content" class="text-muted-foreground">
33
+ <div v-if="content" class="content-accordion-copy">
40
34
  {{ content }}
41
35
  </div>
42
36
  </slot>
43
- <div
44
- v-if="$slots.default"
45
- class="content-prose content-prose-trim px-4 pb-2 text-muted-foreground"
46
- >
37
+ <div v-if="$slots.default" class="content-accordion-copy content-prose content-prose-trim">
47
38
  <slot unwrap="p" />
48
39
  </div>
49
40
  </AccordionContent>
@@ -0,0 +1,159 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, ref, useId } from "vue";
3
+ import { cn } from "../../utils";
4
+ import { apiEntryId, normalizeApiGroups, signatureTail, splitInlineCode } from "./api.utils";
5
+ import { resolveIconifyIcon } from "./icons";
6
+ import { useProseAppearance } from "../../composables/useProseAppearance";
7
+
8
+ const props = defineProps<{
9
+ title?: string;
10
+ icon?: string;
11
+ method?: string;
12
+ path?: string;
13
+ groups?: unknown;
14
+ appearance?: "quiet" | "tint";
15
+ }>();
16
+ const appearance = useProseAppearance("api", () => props.appearance);
17
+
18
+ const groups = computed(() => normalizeApiGroups(props.groups));
19
+ const activeIndex = ref(0);
20
+ const panelId = useId();
21
+
22
+ const iconName = computed(() => resolveIconifyIcon(props.icon));
23
+
24
+ function handleKeydown(event: KeyboardEvent, currentIndex: number) {
25
+ let nextIndex: number | undefined;
26
+ if (event.key === "ArrowRight") nextIndex = (currentIndex + 1) % groups.value.length;
27
+ if (event.key === "ArrowLeft") {
28
+ nextIndex = (currentIndex - 1 + groups.value.length) % groups.value.length;
29
+ }
30
+ if (event.key === "Home") nextIndex = 0;
31
+ if (event.key === "End") nextIndex = groups.value.length - 1;
32
+ if (nextIndex === undefined) return;
33
+
34
+ event.preventDefault();
35
+ activeIndex.value = nextIndex;
36
+ const tabs = (event.currentTarget as HTMLElement).parentElement?.querySelectorAll<HTMLElement>(
37
+ '[role="tab"]',
38
+ );
39
+ tabs?.[nextIndex]?.focus();
40
+ }
41
+
42
+ function applyHash(hash: string) {
43
+ if (!hash) return;
44
+ const index = groups.value.findIndex((group) =>
45
+ group.entries.some((entry) => apiEntryId(group.label, entry.name) === hash),
46
+ );
47
+ if (index === -1) return;
48
+ activeIndex.value = index;
49
+ requestAnimationFrame(() => {
50
+ document.getElementById(hash)?.scrollIntoView();
51
+ });
52
+ }
53
+
54
+ onMounted(() => {
55
+ applyHash(window.location.hash.slice(1));
56
+ });
57
+ </script>
58
+
59
+ <template>
60
+ <div
61
+ v-if="groups.length"
62
+ class="content-api content-tabs not-prose"
63
+ :data-appearance="appearance"
64
+ :data-stacked-header="groups.length > 1 ? 'true' : undefined"
65
+ >
66
+ <div class="content-tabs-header content-api-header">
67
+ <span v-if="method && path" class="content-api-identity">
68
+ <span class="content-api-method">
69
+ {{ method.toUpperCase() }}
70
+ </span>
71
+ <span class="content-api-path">{{ path }}</span>
72
+ </span>
73
+ <span v-else-if="title" class="content-api-identity content-api-title">
74
+ <Icon
75
+ v-if="iconName"
76
+ :name="iconName"
77
+ mode="svg"
78
+ class="content-api-title-icon"
79
+ aria-hidden="true"
80
+ />
81
+ <span class="content-api-path">{{ title }}</span>
82
+ </span>
83
+
84
+ <div class="content-tabs-list content-api-tabs" role="tablist">
85
+ <button
86
+ v-for="(group, index) in groups"
87
+ :key="group.label"
88
+ type="button"
89
+ role="tab"
90
+ :id="`${panelId}-tab-${index}`"
91
+ :aria-selected="activeIndex === index"
92
+ :aria-controls="`${panelId}-${index}`"
93
+ :tabindex="activeIndex === index ? 0 : -1"
94
+ :class="cn('content-tabs-tab', activeIndex === index && 'content-tabs-tab-active')"
95
+ @click="activeIndex = index"
96
+ @keydown="handleKeydown($event, index)"
97
+ >
98
+ {{ group.label }}
99
+ <span class="content-api-count">
100
+ {{ group.entries.length }}
101
+ </span>
102
+ </button>
103
+ </div>
104
+ </div>
105
+
106
+ <div class="content-tabs-panels">
107
+ <div
108
+ v-for="(group, index) in groups"
109
+ :id="`${panelId}-${index}`"
110
+ :key="group.label"
111
+ role="tabpanel"
112
+ :aria-labelledby="`${panelId}-tab-${index}`"
113
+ :style="activeIndex === index ? undefined : { display: 'none' }"
114
+ >
115
+ <div
116
+ v-for="entry in group.entries"
117
+ :id="apiEntryId(group.label, entry.name)"
118
+ :key="entry.name"
119
+ class="content-api-row"
120
+ style="scroll-margin-top: var(--content-scroll-margin)"
121
+ >
122
+ <code class="content-api-signature" :data-entry="entry.name">
123
+ <span
124
+ :class="cn('content-api-name', entry.deprecated && 'content-api-name-deprecated')"
125
+ >{{ entry.name }}</span
126
+ ><span v-if="signatureTail(entry)" class="content-api-type">{{
127
+ signatureTail(entry)
128
+ }}</span>
129
+ </code>
130
+
131
+ <span v-if="entry.required || entry.deprecated || entry.since" class="content-api-badges">
132
+ <span v-if="entry.required" class="content-api-badge" data-state="required">
133
+ required
134
+ </span>
135
+ <span v-if="entry.deprecated" class="content-api-badge" data-state="deprecated">
136
+ deprecated
137
+ </span>
138
+ <span v-if="entry.since" class="content-api-badge" data-state="since">
139
+ since {{ entry.since }}
140
+ </span>
141
+ </span>
142
+
143
+ <!-- A div, not a p: the prose stylesheet's unlayered `.content-prose p`
144
+ rules (1rem margins, inherited 1rem/1.75 type) override layered
145
+ utilities and are not guarded by not-prose. -->
146
+ <div v-if="entry.description" class="content-api-description">
147
+ <template
148
+ v-for="(part, partIndex) in splitInlineCode(entry.description)"
149
+ :key="partIndex"
150
+ >
151
+ <code v-if="part.code" class="content-api-inline-code">{{ part.text }}</code
152
+ ><template v-else>{{ part.text }}</template>
153
+ </template>
154
+ </div>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ </template>
@@ -1,26 +1,22 @@
1
1
  <script setup lang="ts">
2
2
  import type { HTMLAttributes } from "vue";
3
+ import { useProseAppearance } from "../../composables/useProseAppearance";
3
4
  import { cn } from "../../utils";
4
5
 
5
6
  const props = defineProps<{
6
7
  label?: string;
8
+ appearance?: "quiet" | "tint";
7
9
  class?: HTMLAttributes["class"];
8
10
  }>();
11
+ const appearance = useProseAppearance("aside", () => props.appearance);
9
12
  </script>
10
13
 
11
14
  <template>
12
- <aside
13
- :class="
14
- cn(
15
- 'not-prose my-4 rounded-lg border-l-2 border-muted-foreground/20 bg-muted/20 px-4 py-3',
16
- props.class,
17
- )
18
- "
19
- >
20
- <p v-if="label" class="mb-2 text-xs font-medium tracking-wider text-muted-foreground uppercase">
15
+ <aside :data-appearance="appearance" :class="cn('content-aside not-prose', props.class)">
16
+ <p v-if="label" class="content-aside-label">
21
17
  {{ label }}
22
18
  </p>
23
- <div :class="cn('content-prose content-prose-trim text-muted-foreground')">
19
+ <div class="content-aside-body content-prose content-prose-trim">
24
20
  <slot />
25
21
  </div>
26
22
  </aside>
@@ -1,16 +1,18 @@
1
1
  <script setup lang="ts">
2
2
  import type { HTMLAttributes } from "vue";
3
3
  import { computed } from "vue";
4
+ import { useProseAppearance } from "../../composables/useProseAppearance";
4
5
  import { cn } from "../../utils";
5
6
  import { resolveIconifyIcon } from "./icons";
6
7
 
7
- type CalloutType = "note" | "alert" | "info" | "warn" | "warning" | "error" | "success" | "idea";
8
+ type CalloutType = "note" | "info" | "warning" | "error" | "success" | "idea";
8
9
 
9
10
  const props = withDefaults(
10
11
  defineProps<{
11
12
  title?: string;
12
13
  type?: CalloutType;
13
14
  icon?: string;
15
+ appearance?: "quiet" | "tint";
14
16
  class?: HTMLAttributes["class"];
15
17
  }>(),
16
18
  {
@@ -18,11 +20,7 @@ const props = withDefaults(
18
20
  },
19
21
  );
20
22
 
21
- const normalizedType = computed(() => {
22
- if (props.type === "warn" || props.type === "alert") return "warning";
23
-
24
- return props.type;
25
- });
23
+ const appearance = useProseAppearance("callout", () => props.appearance);
26
24
 
27
25
  const iconName = computed(() => {
28
26
  if (props.icon) return resolveIconifyIcon(props.icon) ?? props.icon;
@@ -35,7 +33,7 @@ const iconName = computed(() => {
35
33
  error: "lucide:circle-x",
36
34
  success: "lucide:circle-check",
37
35
  idea: "lucide:lightbulb",
38
- }[normalizedType.value] ?? "lucide:info"
36
+ }[props.type] ?? "lucide:info"
39
37
  );
40
38
  });
41
39
  </script>
@@ -43,12 +41,13 @@ const iconName = computed(() => {
43
41
  <template>
44
42
  <div
45
43
  data-slot="alert"
46
- role="alert"
47
- :class="cn('content-callout not-prose my-4', `content-callout-${normalizedType}`, props.class)"
44
+ :data-appearance="appearance"
45
+ role="note"
46
+ :class="cn('content-callout not-prose my-4', `content-callout-${props.type}`, props.class)"
48
47
  >
49
48
  <span class="content-callout-bar" aria-hidden="true" />
50
49
  <span data-slot="alert-icon" class="content-callout-icon" aria-hidden="true">
51
- <Icon :name="iconName" class="mt-1 size-5" />
50
+ <Icon :name="iconName" />
52
51
  </span>
53
52
  <div class="content-callout-body">
54
53
  <div v-if="title" data-slot="alert-title" class="content-alert-title">
@@ -1,8 +1,16 @@
1
1
  <script setup lang="ts">
2
2
  import { NuxtLink } from "#components";
3
- import { computed, inject, useSlots, type CSSProperties, type HTMLAttributes } from "vue";
3
+ import {
4
+ computed,
5
+ inject,
6
+ useSlots,
7
+ type ComputedRef,
8
+ type CSSProperties,
9
+ type HTMLAttributes,
10
+ } from "vue";
4
11
  import { cn } from "../../utils";
5
12
  import { isExternalLink, resolveIconifyIcon } from "./icons";
13
+ import { useProseAppearance } from "../../composables/useProseAppearance";
6
14
 
7
15
  type CardIconColor =
8
16
  | "muted"
@@ -27,6 +35,7 @@ const props = withDefaults(
27
35
  horizontal?: boolean;
28
36
  inStack?: boolean;
29
37
  class?: HTMLAttributes["class"];
38
+ appearance?: "quiet" | "tint";
30
39
  }>(),
31
40
  {
32
41
  showLinkIcon: true,
@@ -37,6 +46,12 @@ const props = withDefaults(
37
46
 
38
47
  const slots = useSlots();
39
48
  const inGroup = inject("contentCardInGroup", false);
49
+ // Instance prop wins, then the surrounding ::cards tray, then config.
50
+ const groupAppearance = inject<ComputedRef<"quiet" | "tint"> | null>(
51
+ "contentCardsAppearance",
52
+ null,
53
+ );
54
+ const appearance = useProseAppearance("cards", () => props.appearance ?? groupAppearance?.value);
40
55
 
41
56
  const wrapperTag = computed(() => (props.to ? (isExternalLink(props.to) ? "a" : NuxtLink) : "div"));
42
57
 
@@ -93,66 +108,43 @@ const cardIconStyle = computed<CSSProperties | undefined>(() =>
93
108
  <component
94
109
  :is="wrapperTag"
95
110
  v-bind="linkProps || {}"
111
+ :data-appearance="appearance"
96
112
  :class="
97
113
  cn(
98
- 'content-card group not-prose relative block h-auto',
99
- !inStack && !inGroup && 'my-4',
100
- to &&
101
- 'rounded-xl focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none',
114
+ 'content-card group not-prose',
115
+ !inStack && !inGroup && 'content-card-standalone',
116
+ inStack && 'content-card-in-stack',
117
+ to && 'focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none',
102
118
  props.class,
103
119
  )
104
120
  "
105
121
  >
106
- <div
107
- data-slot="card"
108
- :class="
109
- cn(
110
- 'content-card-surface relative overflow-hidden rounded-xl border bg-card text-card-foreground shadow-xs transition-colors',
111
- hasBody || hasFooter ? 'p-0' : 'p-0',
112
- to && 'cursor-pointer hover:bg-accent/50',
113
- showOutgoingLinkIcon && (hasBody || hasFooter ? '' : 'pr-10'),
114
- inStack &&
115
- 'rounded-none border-x-0 shadow-none first:rounded-t-xl first:border-t last:rounded-b-xl last:border-b',
116
- )
117
- "
118
- >
119
- <img v-if="img" :src="img" alt="" class="w-full border-b border-border object-cover" />
122
+ <div data-slot="card" class="content-card-surface">
123
+ <img v-if="img" :src="img" alt="" class="content-card-media" />
120
124
 
121
125
  <div
122
126
  v-if="hasHeader"
123
- :class="
124
- cn(
125
- hasBody || hasFooter ? 'p-4 pb-0' : 'p-4',
126
- showOutgoingLinkIcon && 'pr-10',
127
- horizontal && 'flex items-start gap-3',
128
- )
129
- "
127
+ class="content-card-header"
128
+ :data-trailing="hasBody || hasFooter ? 'true' : undefined"
129
+ :data-horizontal="horizontal ? 'true' : undefined"
130
+ :data-reserve-link-icon="showOutgoingLinkIcon ? 'true' : undefined"
130
131
  >
131
132
  <div
132
133
  v-if="iconName"
133
- :style="cardIconStyle"
134
- :class="
135
- cn(
136
- 'content-card-icon flex size-10 shrink-0 items-center justify-center rounded-lg border bg-muted shadow-xs [&_.iconify]:size-5',
137
- horizontal ? '' : 'mb-2',
138
- )
139
- "
134
+ v-bind="cardIconStyle ? { style: cardIconStyle } : {}"
135
+ class="content-card-icon content-card-tile"
140
136
  >
141
137
  <Icon :name="iconName" aria-hidden="true" />
142
138
  </div>
143
139
 
144
- <div :class="cn('min-w-0', horizontal ? 'flex-1 space-y-1' : 'space-y-1')">
145
- <div
146
- v-if="title || $slots.title"
147
- data-slot="card-title"
148
- :class="cn('text-sm font-medium', !horizontal && 'mb-0')"
149
- >
140
+ <div class="content-card-copy">
141
+ <div v-if="title || $slots.title" data-slot="card-title" class="content-card-title">
150
142
  <slot name="title">{{ title }}</slot>
151
143
  </div>
152
144
  <div
153
145
  v-if="description || $slots.description"
154
146
  data-slot="card-description"
155
- class="text-sm leading-snug text-pretty"
147
+ class="content-card-description"
156
148
  >
157
149
  <slot name="description">{{ description }}</slot>
158
150
  </div>
@@ -162,23 +154,20 @@ const cardIconStyle = computed<CSSProperties | undefined>(() =>
162
154
  <div
163
155
  v-if="hasBody"
164
156
  data-slot="card-content"
165
- class="content-prose content-prose-trim p-4 pt-2 text-muted-foreground"
157
+ class="content-card-body content-prose content-prose-trim"
158
+ :data-reserve-link-icon="showOutgoingLinkIcon && !hasHeader ? 'true' : undefined"
166
159
  >
167
160
  <slot unwrap="p" />
168
161
  </div>
169
162
 
170
- <div
171
- v-if="hasFooter"
172
- data-slot="card-footer"
173
- class="flex flex-col items-stretch border-t px-4 py-3 text-xs text-muted-foreground [&_p]:my-0 [&_p]:leading-normal"
174
- >
163
+ <div v-if="hasFooter" data-slot="card-footer" class="content-card-footer">
175
164
  <slot name="footer">{{ footer }}</slot>
176
165
  </div>
177
166
 
178
167
  <Icon
179
168
  v-if="showOutgoingLinkIcon"
180
169
  name="lucide:arrow-up-right"
181
- class="pointer-events-none absolute top-4 right-4 size-4 text-muted-foreground/50 transition-colors group-hover:text-foreground"
170
+ class="content-card-arrow"
182
171
  aria-hidden="true"
183
172
  />
184
173
  </div>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import { computed, provide, type HTMLAttributes } from "vue";
3
+ import { useProseAppearance } from "../../composables/useProseAppearance";
4
+ import { cn } from "../../utils";
5
+
6
+ const props = withDefaults(
7
+ defineProps<{
8
+ cols?: 1 | 2 | 3 | "1" | "2" | "3";
9
+ appearance?: "quiet" | "tint";
10
+ class?: HTMLAttributes["class"];
11
+ }>(),
12
+ { cols: 2 },
13
+ );
14
+
15
+ provide("contentCardInGroup", true);
16
+ const appearance = useProseAppearance("cards", () => props.appearance);
17
+ // Children must render on the same surface as the tray they sit in.
18
+ provide("contentCardsAppearance", appearance);
19
+ const columns = computed(() => Math.min(3, Math.max(1, Number(props.cols) || 2)));
20
+ </script>
21
+
22
+ <template>
23
+ <div
24
+ :class="cn('content-cards not-prose', props.class)"
25
+ :data-appearance="appearance"
26
+ :data-columns="columns"
27
+ >
28
+ <slot />
29
+ </div>
30
+ </template>
@@ -4,6 +4,7 @@ import { Comment, cloneVNode, computed, h, ref, resolveComponent, useSlots } fro
4
4
  import { useClipboard } from "@vueuse/core";
5
5
  import { useI18n } from "#imports";
6
6
  import { cn } from "../../utils";
7
+ import { useProseAppearance } from "../../composables/useProseAppearance";
7
8
 
8
9
  type CodeBlockProps = {
9
10
  code?: string;
@@ -24,6 +25,8 @@ const slots = useSlots();
24
25
  const activeValue = ref("");
25
26
  const { copy, copied } = useClipboard();
26
27
  const { t } = useI18n();
28
+ const props = defineProps<{ appearance?: "quiet" | "tint" }>();
29
+ const appearance = useProseAppearance("code", () => props.appearance);
27
30
 
28
31
  const PACKAGE_ICONS: Record<string, string> = {
29
32
  bun: "logos:bun",
@@ -113,6 +116,7 @@ function renderCodeGroup() {
113
116
  "div",
114
117
  {
115
118
  class: "content-codegroup not-prose group",
119
+ "data-appearance": appearance.value,
116
120
  },
117
121
  [
118
122
  h("div", { class: "content-codegroup-header" }, [